Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nix/hashes.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-+PJZG5jNxBGkxblpnNa4lvfBi9YEvHaGQRE0+avNwHY="
"nodeModules": "sha256-jLrT8GVq0Fh34tN1MPgJpPKd9SGhOauaBl8f1oZ/XgI="
}
2 changes: 2 additions & 0 deletions packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
"@ai-sdk/mcp": "0.0.8",
"@ai-sdk/openai": "2.0.71",
"@ai-sdk/openai-compatible": "1.0.27",
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.18",
"@clack/prompts": "1.0.0-alpha.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,7 @@ export function Prompt(props: PromptProps) {
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
>
<box flexShrink={0} flexDirection="row" gap={1}>
{/* @ts-ignore // SpinnerOptions doesn't support marginLeft */}
<spinner marginLeft={1} color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
<box flexDirection="row" gap={1} flexShrink={0}>
{(() => {
Expand Down
48 changes: 39 additions & 9 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
import { createOpenAI } from "@ai-sdk/openai"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"

export namespace Provider {
const log = Log.create({ service: "provider" })
Expand All @@ -37,6 +38,8 @@ export namespace Provider {
"@ai-sdk/openai": createOpenAI,
"@ai-sdk/openai-compatible": createOpenAICompatible,
"@openrouter/ai-sdk-provider": createOpenRouter,
// @ts-ignore (TODO: kill this code so we dont have to maintain it)
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
}

type CustomLoader = (provider: ModelsDev.Provider) => Promise<{
Expand Down Expand Up @@ -87,6 +90,30 @@ export namespace Provider {
options: {},
}
},
"github-copilot": async () => {
return {
autoload: false,
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
if (modelID.includes("gpt-5")) {
return sdk.responses(modelID)
}
return sdk.chat(modelID)
},
options: {},
}
},
"github-copilot-enterprise": async () => {
return {
autoload: false,
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
if (modelID.includes("gpt-5")) {
return sdk.responses(modelID)
}
return sdk.chat(modelID)
},
options: {},
}
},
azure: async () => {
return {
autoload: false,
Expand Down Expand Up @@ -428,15 +455,6 @@ export namespace Provider {
}
}

// load custom
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
if (disabled.has(providerID)) continue
const result = await fn(database[providerID])
if (result && (result.autoload || providers[providerID])) {
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
}
}

for (const plugin of await Plugin.list()) {
if (!plugin.auth) continue
const providerID = plugin.auth.provider
Expand Down Expand Up @@ -478,6 +496,14 @@ export namespace Provider {
}
}

for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
if (disabled.has(providerID)) continue
const result = await fn(database[providerID])
if (result && (result.autoload || providers[providerID])) {
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
}
}

// load config
for (const [providerID, provider] of configProviders) {
mergeProvider(providerID, provider.options ?? {}, "config")
Expand All @@ -489,6 +515,10 @@ export namespace Provider {
continue
}

if (providerID === "github-copilot") {
provider.info.npm = "@ai-sdk/github-copilot"
}

const configProvider = config.provider?.[providerID]
const filteredModels = Object.fromEntries(
Object.entries(provider.info.models)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
This is a temporary package used primarily for github copilot compatibility.

Avoid making changes to these files unless you want to only affect Copilot provider.

Also this should ONLY be used for Copilot provider.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { createOpenaiCompatible, openaiCompatible } from "./openai-compatible-provider"
export type { OpenaiCompatibleProvider, OpenaiCompatibleProviderSettings } from "./openai-compatible-provider"
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { LanguageModelV2 } from "@ai-sdk/provider"
import { OpenAICompatibleChatLanguageModel } from "@ai-sdk/openai-compatible"
import { type FetchFunction, withoutTrailingSlash, withUserAgentSuffix } from "@ai-sdk/provider-utils"
import { OpenAIResponsesLanguageModel } from "./responses/openai-responses-language-model"

// Import the version or define it
const VERSION = "0.1.0"

export type OpenaiCompatibleModelId = string

export interface OpenaiCompatibleProviderSettings {
/**
* API key for authenticating requests.
*/
apiKey?: string

/**
* Base URL for the OpenAI Compatible API calls.
*/
baseURL?: string

/**
* Name of the provider.
*/
name?: string

/**
* Custom headers to include in the requests.
*/
headers?: Record<string, string>

/**
* Custom fetch implementation.
*/
fetch?: FetchFunction
}

export interface OpenaiCompatibleProvider {
(modelId: OpenaiCompatibleModelId): LanguageModelV2
chat(modelId: OpenaiCompatibleModelId): LanguageModelV2
responses(modelId: OpenaiCompatibleModelId): LanguageModelV2
languageModel(modelId: OpenaiCompatibleModelId): LanguageModelV2

// embeddingModel(modelId: any): EmbeddingModelV2

// imageModel(modelId: any): ImageModelV2
}

/**
* Create an OpenAI Compatible provider instance.
*/
export function createOpenaiCompatible(options: OpenaiCompatibleProviderSettings = {}): OpenaiCompatibleProvider {
const baseURL = withoutTrailingSlash(options.baseURL ?? "https://api.openai.com/v1")

if (!baseURL) {
throw new Error("baseURL is required")
}

// Merge headers: defaults first, then user overrides
const headers = {
// Default OpenAI Compatible headers (can be overridden by user)
...(options.apiKey && { Authorization: `Bearer ${options.apiKey}` }),
...options.headers,
}

const getHeaders = () => withUserAgentSuffix(headers, `ai-sdk/openai-compatible/${VERSION}`)

const createChatModel = (modelId: OpenaiCompatibleModelId) => {
return new OpenAICompatibleChatLanguageModel(modelId, {
provider: `${options.name ?? "openai-compatible"}.chat`,
headers: getHeaders,
url: ({ path }) => `${baseURL}${path}`,
fetch: options.fetch,
})
}

const createResponsesModel = (modelId: OpenaiCompatibleModelId) => {
return new OpenAIResponsesLanguageModel(modelId, {
provider: `${options.name ?? "openai-compatible"}.responses`,
headers: getHeaders,
url: ({ path }) => `${baseURL}${path}`,
fetch: options.fetch,
})
}

const createLanguageModel = (modelId: OpenaiCompatibleModelId) => createChatModel(modelId)

const provider = function (modelId: OpenaiCompatibleModelId) {
return createChatModel(modelId)
}

provider.languageModel = createLanguageModel
provider.chat = createChatModel
provider.responses = createResponsesModel

return provider as OpenaiCompatibleProvider
}

// Default OpenAI Compatible provider instance
export const openaiCompatible = createOpenaiCompatible()
Loading