mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-08 19:18:47 +00:00
feat(ai): Responses routes for OpenRouter, DigitalOcean and Venice (#59991)
This commit is contained in:
parent
da702a85a6
commit
dd35fbc2f4
22 changed files with 623 additions and 47 deletions
|
|
@ -206,7 +206,7 @@ const everything = AIClient.layerWith(logged) // or AI.make({ layer: logged })
|
|||
## Venice AI
|
||||
|
||||
`Venice` provides native Chat Completions with streaming tools and reasoning. `model` and `chat`
|
||||
select the same API; credentials default to `VENICE_API_KEY`.
|
||||
select the same API, and `responses` selects its Responses API; credentials default to `VENICE_API_KEY`.
|
||||
|
||||
```ts
|
||||
import { LLM } from "@opencode/ai/effect"
|
||||
|
|
@ -235,8 +235,8 @@ signatures, including per-tool signatures. Venice's encrypted scalar trailers ar
|
|||
reasoning but retained in provider metadata for replay. Cache affinity uses `promptCacheKey`, and cache-write
|
||||
usage reads Venice's `cache_creation_input_tokens` field.
|
||||
|
||||
The native package entrypoint is `@opencode/ai/providers/venice`. Image generation, embeddings, Responses, and
|
||||
client-side E2EE are not implemented by this provider.
|
||||
The native package entrypoints are `@opencode/ai/providers/venice` and `@opencode/ai/providers/venice/responses`.
|
||||
Image generation, embeddings, and client-side E2EE are not implemented by this provider.
|
||||
|
||||
## Decisions
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
"./providers/digitalocean": "./src/providers/digitalocean.ts",
|
||||
"./providers/digitalocean/decisions": "./src/providers/digitalocean/decisions.ts",
|
||||
"./providers/digitalocean/images": "./src/providers/digitalocean/images.ts",
|
||||
"./providers/digitalocean/responses": "./src/providers/digitalocean/responses.ts",
|
||||
"./providers/elevenlabs": "./src/providers/elevenlabs.ts",
|
||||
"./providers/fal": "./src/providers/fal.ts",
|
||||
"./providers/fireworks": "./src/providers/fireworks.ts",
|
||||
|
|
@ -100,6 +101,7 @@
|
|||
"./providers/openrouter": "./src/providers/openrouter.ts",
|
||||
"./providers/openrouter/decisions": "./src/providers/openrouter/decisions.ts",
|
||||
"./providers/openrouter/images": "./src/providers/openrouter/images.ts",
|
||||
"./providers/openrouter/responses": "./src/providers/openrouter/responses.ts",
|
||||
"./providers/replicate": "./src/providers/replicate.ts",
|
||||
"./providers/runway": "./src/providers/runway.ts",
|
||||
"./providers/stability": "./src/providers/stability.ts",
|
||||
|
|
@ -107,6 +109,7 @@
|
|||
"./providers/typesafe-ai": "./src/providers/typesafe-ai.ts",
|
||||
"./providers/typesafe-ai/decisions": "./src/providers/typesafe-ai/decisions.ts",
|
||||
"./providers/venice": "./src/providers/venice.ts",
|
||||
"./providers/venice/responses": "./src/providers/venice/responses.ts",
|
||||
"./providers/vercel-ai-gateway": "./src/providers/vercel-ai-gateway.ts",
|
||||
"./providers/vercel-ai-gateway/chat": "./src/providers/vercel-ai-gateway/chat.ts",
|
||||
"./providers/vercel-ai-gateway/decisions": "./src/providers/vercel-ai-gateway/decisions.ts",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { OpenAIImages } from "../protocols/openai-images.js"
|
||||
import { OpenResponses } from "../protocols/open-responses.js"
|
||||
import { SystemOne } from "../protocols/system-one.js"
|
||||
import { cacheControl } from "../protocols/utils/cache.js"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
|
|
@ -10,6 +11,7 @@ import { MediaRoute } from "../route/media.js"
|
|||
import { Protocol } from "../route/protocol.js"
|
||||
import { ProviderID, type ModelID, type ProviderOptions } from "../schema/index.js"
|
||||
import type { OpenAIProviderOptionsInput } from "./openai-options.js"
|
||||
import type { OpenResponsesProviderOptionsInput } from "./open-responses-options.js"
|
||||
|
||||
export const id = ProviderID.make("digitalocean")
|
||||
const baseURL = "https://inference.do-ai.run/v1"
|
||||
|
|
@ -17,10 +19,11 @@ const baseURL = "https://inference.do-ai.run/v1"
|
|||
export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput | OpenResponsesProviderOptionsInput
|
||||
}
|
||||
|
||||
export type Settings = ProviderPackage.Settings & OpenAIProviderOptionsInput & { readonly apiKey?: string }
|
||||
export type Settings<Options = OpenAIProviderOptionsInput> = ProviderPackage.Settings &
|
||||
Options & { readonly apiKey?: string }
|
||||
|
||||
export const protocol = Protocol.make({
|
||||
id: "digitalocean-chat",
|
||||
|
|
@ -40,16 +43,24 @@ export const route = Route.make({
|
|||
framing: OpenAIChat.framing,
|
||||
})
|
||||
|
||||
export const routes = [route]
|
||||
const responsesRoute = Route.make({
|
||||
id: "digitalocean-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "digitalocean",
|
||||
protocol: OpenResponses.protocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL }),
|
||||
transport: OpenResponses.httpTransport,
|
||||
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
|
||||
})
|
||||
|
||||
export const routes = [route, responsesRoute]
|
||||
|
||||
export const configure = (input: Config = {}) => {
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL: endpoint, ...defaults } = input
|
||||
const auth = AuthOptions.bearer(input, ["DIGITALOCEAN_ACCESS_TOKEN", "DIGITALOCEAN_API_KEY", "DO_INFERENCE_API_KEY"])
|
||||
const configured = route.with({
|
||||
...defaults,
|
||||
endpoint: { baseURL: endpoint ?? baseURL },
|
||||
auth,
|
||||
})
|
||||
const options = { ...defaults, endpoint: { baseURL: endpoint ?? baseURL }, auth }
|
||||
const configured = route.with(options)
|
||||
const configuredResponses = responsesRoute.with(options)
|
||||
const decisionRoute = SystemOne.route.with({
|
||||
provider: id,
|
||||
providerMetadataKey: "digitalocean",
|
||||
|
|
@ -73,6 +84,8 @@ export const configure = (input: Config = {}) => {
|
|||
supportsPromptCacheKey: true,
|
||||
},
|
||||
}),
|
||||
responses: (modelID: string | ModelID) =>
|
||||
configuredResponses.model<OpenResponsesProviderOptionsInput>({ id: modelID }),
|
||||
image,
|
||||
decision,
|
||||
configure,
|
||||
|
|
@ -82,9 +95,14 @@ export const configure = (input: Config = {}) => {
|
|||
export const provider = configure()
|
||||
export const decision = provider.decision
|
||||
export const image = provider.image
|
||||
export const responses = provider.responses
|
||||
|
||||
export const model: ProviderPackage.Model<Settings, OpenAIProviderOptionsInput> = (modelID, settings) =>
|
||||
configure(config(settings)).model(modelID)
|
||||
export const responsesModel: ProviderPackage.Model<
|
||||
Settings<OpenResponsesProviderOptionsInput>,
|
||||
OpenResponsesProviderOptionsInput
|
||||
> = (modelID, settings) => configure(config(settings)).responses(modelID)
|
||||
export const imageModel = (modelID: string, settings?: Settings) => {
|
||||
const input = config(settings)
|
||||
return configure(input).image(modelID, input.providerOptions)
|
||||
|
|
@ -94,7 +112,7 @@ export const decisionModel = (modelID: string, settings?: Settings) => {
|
|||
return configure(input).decision(modelID, input.providerOptions)
|
||||
}
|
||||
|
||||
function config({ apiKey, baseURL, body, headers, ...providerOptions }: Settings = {}) {
|
||||
function config({ apiKey, baseURL, body, headers, ...providerOptions }: Settings<Config["providerOptions"]> = {}) {
|
||||
return {
|
||||
apiKey,
|
||||
baseURL,
|
||||
|
|
|
|||
4
packages/ai/src/providers/digitalocean/responses.ts
Normal file
4
packages/ai/src/providers/digitalocean/responses.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import type { OpenResponsesProviderOptionsInput } from "../open-responses-options.js"
|
||||
import type { DigitalOcean } from "../digitalocean.js"
|
||||
export { responsesModel as model } from "../digitalocean.js"
|
||||
export type Settings = DigitalOcean.Settings<OpenResponsesProviderOptionsInput>
|
||||
|
|
@ -9,8 +9,10 @@ import { SystemOne } from "../protocols/system-one.js"
|
|||
import { OpenAIImages } from "../protocols/openai-images.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { OpenResponses } from "../protocols/open-responses.js"
|
||||
import { cacheControl } from "../protocols/utils/cache.js"
|
||||
import { isRecord, JsonObject, lenient, ProviderShared } from "../protocols/shared.js"
|
||||
import type { OpenResponsesProviderOptionsInput } from "./open-responses-options.js"
|
||||
|
||||
export const id = ProviderID.make("openrouter")
|
||||
const baseURL = "https://openrouter.ai/api/v1"
|
||||
|
|
@ -83,11 +85,11 @@ export interface OpenRouterDecisionOptions {
|
|||
export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: OpenRouterProviderOptionsInput
|
||||
readonly providerOptions?: OpenRouterProviderOptionsInput | OpenResponsesProviderOptionsInput
|
||||
}
|
||||
|
||||
export type Settings = ProviderPackage.Settings &
|
||||
OpenRouterProviderOptionsInput & {
|
||||
export type Settings<Options = OpenRouterProviderOptionsInput> = ProviderPackage.Settings &
|
||||
Options & {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL?: string
|
||||
}
|
||||
|
|
@ -183,20 +185,24 @@ export const route = Route.make({
|
|||
framing: OpenAIChat.framing,
|
||||
})
|
||||
|
||||
export const routes = [route]
|
||||
const responsesRoute = Route.make({
|
||||
id: "openrouter-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "openrouter",
|
||||
protocol: OpenResponses.protocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL }),
|
||||
transport: OpenResponses.httpTransport,
|
||||
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
|
||||
})
|
||||
|
||||
const configuredRoute = (input: Config) => {
|
||||
const { apiKey: _, auth: _auth, baseURL: endpoint, ...rest } = input
|
||||
return route.with({
|
||||
...rest,
|
||||
endpoint: { baseURL: endpoint ?? baseURL },
|
||||
auth: AuthOptions.bearer(input, "OPENROUTER_API_KEY"),
|
||||
})
|
||||
}
|
||||
export const routes = [route, responsesRoute]
|
||||
|
||||
export const configure = (input: Config = {}) => {
|
||||
const route = configuredRoute(input)
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL: endpoint, ...defaults } = input
|
||||
const auth = AuthOptions.bearer(input, "OPENROUTER_API_KEY")
|
||||
const options = { ...defaults, endpoint: { baseURL: endpoint ?? baseURL }, auth }
|
||||
const chatRoute = route.with(options)
|
||||
const configuredResponses = responsesRoute.with(options)
|
||||
const decisionRoute = SystemOne.route.with({
|
||||
provider: id,
|
||||
providerMetadataKey: "openrouter",
|
||||
|
|
@ -217,7 +223,9 @@ export const configure = (input: Config = {}) => {
|
|||
return {
|
||||
id,
|
||||
model: (modelID: string | ModelID) =>
|
||||
route.model<OpenRouterProviderOptionsInput>({ id: modelID, compatibility: { supportsPromptCacheKey: true } }),
|
||||
chatRoute.model<OpenRouterProviderOptionsInput>({ id: modelID, compatibility: { supportsPromptCacheKey: true } }),
|
||||
responses: (modelID: string | ModelID) =>
|
||||
configuredResponses.model<OpenResponsesProviderOptionsInput>({ id: modelID }),
|
||||
image,
|
||||
decision,
|
||||
configure,
|
||||
|
|
@ -227,8 +235,13 @@ export const configure = (input: Config = {}) => {
|
|||
export const provider = configure()
|
||||
export const decision = provider.decision
|
||||
export const image = provider.image
|
||||
export const responses = provider.responses
|
||||
export const model: ProviderPackage.Model<Settings, OpenRouterProviderOptionsInput> = (modelID, settings) =>
|
||||
configure(config(settings)).model(modelID)
|
||||
export const responsesModel: ProviderPackage.Model<
|
||||
Settings<OpenResponsesProviderOptionsInput>,
|
||||
OpenResponsesProviderOptionsInput
|
||||
> = (modelID, settings) => configure(config(settings)).responses(modelID)
|
||||
export const imageModel = (modelID: string, settings?: Settings) => {
|
||||
const input = config(settings)
|
||||
return configure(input).image(modelID, input.providerOptions)
|
||||
|
|
@ -238,7 +251,7 @@ export const decisionModel = (modelID: string, settings?: Settings) => {
|
|||
return configure(input).decision(modelID, input.providerOptions)
|
||||
}
|
||||
|
||||
function config({ apiKey, baseURL, body, headers, ...providerOptions }: Settings = {}) {
|
||||
function config({ apiKey, baseURL, body, headers, ...providerOptions }: Settings<Config["providerOptions"]> = {}) {
|
||||
return {
|
||||
apiKey,
|
||||
baseURL,
|
||||
|
|
@ -247,3 +260,5 @@ function config({ apiKey, baseURL, body, headers, ...providerOptions }: Settings
|
|||
providerOptions,
|
||||
}
|
||||
}
|
||||
|
||||
export * as OpenRouter from "./openrouter.js"
|
||||
|
|
|
|||
4
packages/ai/src/providers/openrouter/responses.ts
Normal file
4
packages/ai/src/providers/openrouter/responses.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import type { OpenResponsesProviderOptionsInput } from "../open-responses-options.js"
|
||||
import type { OpenRouter } from "../openrouter.js"
|
||||
export { responsesModel as model } from "../openrouter.js"
|
||||
export type Settings = OpenRouter.Settings<OpenResponsesProviderOptionsInput>
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { VeniceChat } from "../protocols/venice-chat.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { OpenResponses } from "../protocols/open-responses.js"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
import { Route, type RouteDefaultsInput } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { ProviderID, type ModelID } from "../schema/index.js"
|
||||
import type { OpenResponsesProviderOptionsInput } from "./open-responses-options.js"
|
||||
|
||||
export const id = ProviderID.make("venice")
|
||||
export type ChatOptionsInput = VeniceChat.OptionsInput
|
||||
|
|
@ -12,51 +14,76 @@ export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
|
|||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly queryParams?: Readonly<Record<string, string>>
|
||||
readonly providerOptions?: ChatOptionsInput
|
||||
readonly providerOptions?: ChatOptionsInput | OpenResponsesProviderOptionsInput
|
||||
}
|
||||
export type Settings = ProviderPackage.Settings &
|
||||
ChatOptionsInput & {
|
||||
export type Settings<Options = ChatOptionsInput> = ProviderPackage.Settings &
|
||||
Options & {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL?: string
|
||||
readonly queryParams?: Readonly<Record<string, string>>
|
||||
}
|
||||
|
||||
const baseURL = "https://api.venice.ai/api/v1"
|
||||
|
||||
const route = Route.make({
|
||||
id: "venice-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "venice",
|
||||
protocol: VeniceChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL: "https://api.venice.ai/api/v1" }),
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
})
|
||||
export const routes = [route]
|
||||
const responsesRoute = Route.make({
|
||||
id: "venice-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "venice",
|
||||
protocol: OpenResponses.protocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL }),
|
||||
transport: OpenResponses.httpTransport,
|
||||
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
|
||||
})
|
||||
export const routes = [route, responsesRoute]
|
||||
|
||||
export const configure = (input: Config = {}) => {
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL, queryParams, ...rest } = input
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL: endpoint, queryParams, ...rest } = input
|
||||
const options = {
|
||||
...rest,
|
||||
endpoint: { baseURL: endpoint ?? baseURL, query: queryParams },
|
||||
auth: AuthOptions.bearer(input, "VENICE_API_KEY"),
|
||||
}
|
||||
const chat = (modelID: string | ModelID) =>
|
||||
route
|
||||
.with({
|
||||
...rest,
|
||||
endpoint: { baseURL: baseURL ?? route.endpoint.baseURL, query: queryParams },
|
||||
auth: AuthOptions.bearer(input, "VENICE_API_KEY"),
|
||||
})
|
||||
.model<ChatOptionsInput>({ id: modelID, compatibility: VeniceChat.compatibility })
|
||||
return { id, model: chat, chat, configure }
|
||||
route.with(options).model<ChatOptionsInput>({ id: modelID, compatibility: VeniceChat.compatibility })
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
responsesRoute.with(options).model<OpenResponsesProviderOptionsInput>({ id: modelID })
|
||||
return { id, model: chat, chat, responses, configure }
|
||||
}
|
||||
|
||||
export const provider = configure()
|
||||
export const chat = provider.chat
|
||||
export const model: ProviderPackage.Model<Settings, ChatOptionsInput> = (
|
||||
modelID,
|
||||
{ apiKey, baseURL, queryParams, body, headers, ...providerOptions }: Settings = {},
|
||||
) =>
|
||||
configure({
|
||||
export const responses = provider.responses
|
||||
export const model: ProviderPackage.Model<Settings, ChatOptionsInput> = (modelID, settings) =>
|
||||
configure(config(settings)).model(modelID)
|
||||
export const responsesModel: ProviderPackage.Model<
|
||||
Settings<OpenResponsesProviderOptionsInput>,
|
||||
OpenResponsesProviderOptionsInput
|
||||
> = (modelID, settings) => configure(config(settings)).responses(modelID)
|
||||
|
||||
function config({
|
||||
apiKey,
|
||||
baseURL,
|
||||
queryParams,
|
||||
body,
|
||||
headers,
|
||||
...providerOptions
|
||||
}: Settings<Config["providerOptions"]> = {}) {
|
||||
return {
|
||||
apiKey,
|
||||
baseURL,
|
||||
queryParams,
|
||||
headers,
|
||||
http: body === undefined ? undefined : { body },
|
||||
providerOptions,
|
||||
}).model(modelID)
|
||||
}
|
||||
}
|
||||
|
||||
export * as Venice from "./venice.js"
|
||||
|
|
|
|||
4
packages/ai/src/providers/venice/responses.ts
Normal file
4
packages/ai/src/providers/venice/responses.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import type { OpenResponsesProviderOptionsInput } from "../open-responses-options.js"
|
||||
import type { Venice } from "../venice.js"
|
||||
export { responsesModel as model } from "../venice.js"
|
||||
export type Settings = Venice.Settings<OpenResponsesProviderOptionsInput>
|
||||
35
packages/ai/test/fixtures/recordings/digitalocean-responses/calls-a-freeform-text-tool.json
vendored
Normal file
35
packages/ai/test/fixtures/recordings/digitalocean-responses/calls-a-freeform-text-tool.json
vendored
Normal file
File diff suppressed because one or more lines are too long
53
packages/ai/test/fixtures/recordings/digitalocean-responses/continues-a-tool-call.json
vendored
Normal file
53
packages/ai/test/fixtures/recordings/digitalocean-responses/continues-a-tool-call.json
vendored
Normal file
File diff suppressed because one or more lines are too long
35
packages/ai/test/fixtures/recordings/digitalocean-responses/streams-text.json
vendored
Normal file
35
packages/ai/test/fixtures/recordings/digitalocean-responses/streams-text.json
vendored
Normal file
File diff suppressed because one or more lines are too long
35
packages/ai/test/fixtures/recordings/openrouter-responses/calls-a-freeform-text-tool.json
vendored
Normal file
35
packages/ai/test/fixtures/recordings/openrouter-responses/calls-a-freeform-text-tool.json
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "openai/gpt-5.4-mini",
|
||||
"tags": [
|
||||
"prefix:openrouter-responses",
|
||||
"provider:openrouter",
|
||||
"protocol:open-responses",
|
||||
"tool",
|
||||
"freeform"
|
||||
],
|
||||
"name": "openrouter-responses/calls-a-freeform-text-tool",
|
||||
"recordedAt": "2026-10-28T19:12:31.652Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://openrouter.ai/api/v1/responses",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"openai/gpt-5.4-mini\",\"input\":[{\"type\":\"message\",\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Use the run tool to execute exactly this code: print(1)\"}]}],\"stream\":true,\"max_output_tokens\":256,\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"tools\":[{\"type\":\"custom\",\"name\":\"run\",\"description\":\"Run a Python snippet.\",\"format\":{\"type\":\"text\"}}],\"tool_choice\":\"required\"}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"type\":\"response.created\",\"response\":{\"id\":\"gen-1793214750-mKxSP1x7cfTqz0InQBR1\",\"object\":\"response\",\"created_at\":1793214750,\"model\":\"openai/gpt-5.4-mini\",\"status\":\"in_progress\",\"completed_at\":null,\"output\":[],\"error\":null,\"incomplete_details\":null,\"tools\":[{\"type\":\"custom\",\"name\":\"run\",\"description\":\"Run a Python snippet.\",\"format\":{\"type\":\"text\"}}],\"tool_choice\":\"required\",\"parallel_tool_calls\":true,\"max_output_tokens\":256,\"temperature\":1,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"auto\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":null,\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":null},\"sequence_number\":0}\n\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"gen-1793214750-mKxSP1x7cfTqz0InQBR1\",\"object\":\"response\",\"created_at\":1793214750,\"model\":\"openai/gpt-5.4-mini\",\"status\":\"in_progress\",\"completed_at\":null,\"output\":[],\"error\":null,\"incomplete_details\":null,\"tools\":[{\"type\":\"custom\",\"name\":\"run\",\"description\":\"Run a Python snippet.\",\"format\":{\"type\":\"text\"}}],\"tool_choice\":\"required\",\"parallel_tool_calls\":true,\"max_output_tokens\":256,\"temperature\":1,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"auto\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":null,\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":null},\"sequence_number\":1}\n\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"id\":\"ctc_tmp_aasptvj0fik\",\"type\":\"custom_tool_call\",\"status\":\"in_progress\",\"call_id\":\"call_jlLLytaby5F4w22U0yMR8kvU\",\"name\":\"run\",\"input\":\"\"},\"sequence_number\":2}\n\ndata: {\"type\":\"response.custom_tool_call_input.delta\",\"output_index\":0,\"item_id\":\"ctc_tmp_aasptvj0fik\",\"delta\":\"print\",\"sequence_number\":3}\n\ndata: {\"type\":\"response.custom_tool_call_input.delta\",\"output_index\":0,\"item_id\":\"ctc_tmp_aasptvj0fik\",\"delta\":\"(\",\"sequence_number\":4}\n\ndata: {\"type\":\"response.custom_tool_call_input.delta\",\"output_index\":0,\"item_id\":\"ctc_tmp_aasptvj0fik\",\"delta\":\"1\",\"sequence_number\":5}\n\ndata: {\"type\":\"response.custom_tool_call_input.delta\",\"output_index\":0,\"item_id\":\"ctc_tmp_aasptvj0fik\",\"delta\":\")\",\"sequence_number\":6}\n\ndata: {\"type\":\"response.custom_tool_call_input.done\",\"output_index\":0,\"item_id\":\"ctc_tmp_aasptvj0fik\",\"input\":\"print(1)\",\"sequence_number\":7}\n\ndata: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"id\":\"ctc_tmp_aasptvj0fik\",\"type\":\"custom_tool_call\",\"status\":\"completed\",\"call_id\":\"call_jlLLytaby5F4w22U0yMR8kvU\",\"name\":\"run\",\"input\":\"print(1)\"},\"sequence_number\":8}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"gen-1793214750-mKxSP1x7cfTqz0InQBR1\",\"object\":\"response\",\"created_at\":1793214750,\"model\":\"openai/gpt-5.4-mini\",\"status\":\"completed\",\"completed_at\":1793214751,\"output\":[{\"id\":\"ctc_tmp_aasptvj0fik\",\"type\":\"custom_tool_call\",\"status\":\"completed\",\"call_id\":\"call_jlLLytaby5F4w22U0yMR8kvU\",\"name\":\"run\",\"input\":\"print(1)\"}],\"error\":null,\"incomplete_details\":null,\"tools\":[{\"type\":\"custom\",\"name\":\"run\",\"description\":\"Run a Python snippet.\",\"format\":{\"type\":\"text\"}}],\"tool_choice\":\"required\",\"parallel_tool_calls\":true,\"max_output_tokens\":256,\"temperature\":1,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":null,\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":{\"input_tokens\":64,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":14,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":78,\"cost\":0.000111,\"is_byok\":false,\"cost_details\":{\"upstream_inference_cost\":0.000111,\"upstream_inference_input_cost\":0.000048,\"upstream_inference_output_cost\":0.000063}}},\"sequence_number\":9}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
53
packages/ai/test/fixtures/recordings/openrouter-responses/continues-a-tool-call.json
vendored
Normal file
53
packages/ai/test/fixtures/recordings/openrouter-responses/continues-a-tool-call.json
vendored
Normal file
File diff suppressed because one or more lines are too long
35
packages/ai/test/fixtures/recordings/openrouter-responses/streams-text.json
vendored
Normal file
35
packages/ai/test/fixtures/recordings/openrouter-responses/streams-text.json
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "openai/gpt-5.4-mini",
|
||||
"tags": [
|
||||
"prefix:openrouter-responses",
|
||||
"provider:openrouter",
|
||||
"protocol:open-responses",
|
||||
"text",
|
||||
"usage"
|
||||
],
|
||||
"name": "openrouter-responses/streams-text",
|
||||
"recordedAt": "2026-10-28T19:11:59.562Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://openrouter.ai/api/v1/responses",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"openai/gpt-5.4-mini\",\"input\":[{\"type\":\"message\",\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: OK\"}]}],\"stream\":true,\"max_output_tokens\":64,\"store\":false,\"include\":[\"reasoning.encrypted_content\"]}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"type\":\"response.created\",\"response\":{\"id\":\"gen-1793214718-hQ61zZmJsMM3exScOj2w\",\"object\":\"response\",\"created_at\":1793214718,\"model\":\"openai/gpt-5.4-mini\",\"status\":\"in_progress\",\"completed_at\":null,\"output\":[],\"error\":null,\"incomplete_details\":null,\"tools\":[],\"tool_choice\":\"auto\",\"parallel_tool_calls\":true,\"max_output_tokens\":64,\"temperature\":1,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"auto\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":null,\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":null},\"sequence_number\":0}\n\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"gen-1793214718-hQ61zZmJsMM3exScOj2w\",\"object\":\"response\",\"created_at\":1793214718,\"model\":\"openai/gpt-5.4-mini\",\"status\":\"in_progress\",\"completed_at\":null,\"output\":[],\"error\":null,\"incomplete_details\":null,\"tools\":[],\"tool_choice\":\"auto\",\"parallel_tool_calls\":true,\"max_output_tokens\":64,\"temperature\":1,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"auto\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":null,\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":null},\"sequence_number\":1}\n\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"id\":\"msg_tmp_luc1vnm9au\",\"type\":\"message\",\"status\":\"in_progress\",\"role\":\"assistant\",\"content\":[],\"phase\":\"final_answer\"},\"sequence_number\":2}\n\ndata: {\"type\":\"response.content_part.added\",\"output_index\":0,\"item_id\":\"msg_tmp_luc1vnm9au\",\"content_index\":0,\"part\":{\"type\":\"output_text\",\"text\":\"\",\"annotations\":[],\"logprobs\":[]},\"sequence_number\":3}\n\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"item_id\":\"msg_tmp_luc1vnm9au\",\"content_index\":0,\"delta\":\"OK\",\"logprobs\":[],\"sequence_number\":4}\n\ndata: {\"type\":\"response.output_text.done\",\"output_index\":0,\"item_id\":\"msg_tmp_luc1vnm9au\",\"content_index\":0,\"text\":\"OK\",\"logprobs\":[],\"sequence_number\":5}\n\ndata: {\"type\":\"response.content_part.done\",\"output_index\":0,\"item_id\":\"msg_tmp_luc1vnm9au\",\"content_index\":0,\"part\":{\"type\":\"output_text\",\"text\":\"OK\",\"annotations\":[],\"logprobs\":[]},\"sequence_number\":6}\n\ndata: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"id\":\"msg_tmp_luc1vnm9au\",\"type\":\"message\",\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"OK\",\"annotations\":[],\"logprobs\":[]}],\"phase\":\"final_answer\"},\"sequence_number\":7}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"gen-1793214718-hQ61zZmJsMM3exScOj2w\",\"object\":\"response\",\"created_at\":1793214718,\"model\":\"openai/gpt-5.4-mini\",\"status\":\"completed\",\"completed_at\":1793214719,\"output\":[{\"id\":\"msg_tmp_luc1vnm9au\",\"type\":\"message\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"OK\",\"annotations\":[],\"logprobs\":[]}],\"phase\":\"final_answer\"}],\"error\":null,\"incomplete_details\":null,\"tools\":[],\"tool_choice\":\"auto\",\"parallel_tool_calls\":true,\"max_output_tokens\":64,\"temperature\":1,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":null,\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":{\"input_tokens\":10,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":5,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":15,\"cost\":0.00003,\"is_byok\":false,\"cost_details\":{\"upstream_inference_cost\":0.00003,\"upstream_inference_input_cost\":0.0000075,\"upstream_inference_output_cost\":0.0000225}}},\"sequence_number\":8}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
35
packages/ai/test/fixtures/recordings/venice-responses/calls-a-freeform-text-tool.json
vendored
Normal file
35
packages/ai/test/fixtures/recordings/venice-responses/calls-a-freeform-text-tool.json
vendored
Normal file
File diff suppressed because one or more lines are too long
53
packages/ai/test/fixtures/recordings/venice-responses/continues-a-tool-call.json
vendored
Normal file
53
packages/ai/test/fixtures/recordings/venice-responses/continues-a-tool-call.json
vendored
Normal file
File diff suppressed because one or more lines are too long
35
packages/ai/test/fixtures/recordings/venice-responses/streams-text.json
vendored
Normal file
35
packages/ai/test/fixtures/recordings/venice-responses/streams-text.json
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -58,6 +58,37 @@ describe("provider package entrypoints", () => {
|
|||
})
|
||||
})
|
||||
|
||||
test("maps OpenRouter, DigitalOcean and Venice Responses entrypoints onto provider-owned routes", async () => {
|
||||
const providers = [
|
||||
["openrouter", "https://openrouter.ai/api/v1", await import("@opencode/ai/providers/openrouter/responses")],
|
||||
["digitalocean", "https://inference.do-ai.run/v1", await import("@opencode/ai/providers/digitalocean/responses")],
|
||||
["venice", "https://api.venice.ai/api/v1", await import("@opencode/ai/providers/venice/responses")],
|
||||
] as const
|
||||
const settings = {
|
||||
apiKey: "fixture",
|
||||
headers: { "x-test": "fixture" },
|
||||
body: { future_option: true },
|
||||
reasoningEffort: "low",
|
||||
}
|
||||
for (const [provider, baseURL, module] of providers) {
|
||||
const selected = module.model("gpt-5.4", settings)
|
||||
expect(selected.provider).toBe(provider)
|
||||
expect(selected.route.id).toBe(`${provider}-responses`)
|
||||
expect(selected.route.protocol).toBe("open-responses")
|
||||
expect(selected.route.endpoint).toMatchObject({ baseURL, path: "/responses" })
|
||||
expect(selected.route.defaults.headers).toEqual(settings.headers)
|
||||
expect(selected.route.defaults.http?.body).toEqual(settings.body)
|
||||
expect(selected.route.defaults.providerOptions).toEqual({
|
||||
reasoningEffort: "low",
|
||||
store: false,
|
||||
include: ["reasoning.encrypted_content"],
|
||||
})
|
||||
expect(module.model("gpt-5.4", { ...settings, baseURL: "https://gateway.test/v1" }).route.endpoint.baseURL).toBe(
|
||||
"https://gateway.test/v1",
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test("maps Cohere entrypoints onto native and compatibility routes", async () => {
|
||||
const modules = await Promise.all([
|
||||
import("@opencode/ai/providers/cohere"),
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@ import {
|
|||
import * as AmazonBedrockMantle from "../../src/providers/amazon-bedrock-mantle.js"
|
||||
import * as Anthropic from "../../src/providers/anthropic.js"
|
||||
import * as Azure from "../../src/providers/azure.js"
|
||||
import * as DigitalOcean from "../../src/providers/digitalocean.js"
|
||||
import * as OpenAI from "../../src/providers/openai.js"
|
||||
import * as OpenAICompatible from "../../src/providers/openai-compatible.js"
|
||||
import * as OpenAICompatibleResponses from "../../src/providers/openai-compatible-responses.js"
|
||||
import * as OpenRouter from "../../src/providers/openrouter.js"
|
||||
import * as Venice from "../../src/providers/venice.js"
|
||||
import * as VercelAIGateway from "../../src/providers/vercel-ai-gateway.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
|
|
@ -112,6 +114,9 @@ describe("freeform tools", () => {
|
|||
AmazonBedrockMantle.responsesModel("openai.gpt-5.4", { ...settings, region: "us-east-1" }),
|
||||
VercelAIGateway.responsesModel("openai/gpt-5.4", settings),
|
||||
OpenAICompatibleResponses.model("gpt-5.4", { ...settings, baseURL: "https://responses.test/v1" }),
|
||||
OpenRouter.responsesModel("openai/gpt-5.4", settings),
|
||||
DigitalOcean.responsesModel("openai-gpt-5.4", settings),
|
||||
Venice.responsesModel("openai-gpt-54", settings),
|
||||
]
|
||||
const chatModels = [
|
||||
Azure.chatModel("gpt-5.4", { ...settings, resourceName: "fixture" }),
|
||||
|
|
|
|||
90
packages/ai/test/provider/responses-routes.recorded.test.ts
Normal file
90
packages/ai/test/provider/responses-routes.recorded.test.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { expect } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { LLM, LLMEvent, toDefinitions } from "../../src/effect.js"
|
||||
import { DigitalOcean, OpenRouter, Venice } from "../../src/providers.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
import { Tool } from "../../src/tool.js"
|
||||
import { expectWeatherToolLoop, runWeatherToolLoop, weatherToolLoopRequest } from "../recorded-scenarios.js"
|
||||
import { recordedTests } from "../recorded-test.js"
|
||||
|
||||
const run = toDefinitions({
|
||||
run: Tool.make({
|
||||
description: "Run a Python snippet.",
|
||||
parameters: Schema.Struct({ code: Schema.String }),
|
||||
success: Schema.String,
|
||||
freeform: { type: "text" },
|
||||
}),
|
||||
})
|
||||
|
||||
for (const item of [
|
||||
{
|
||||
provider: "openrouter",
|
||||
env: "OPENROUTER_API_KEY",
|
||||
model: OpenRouter.configure({ apiKey: process.env.OPENROUTER_API_KEY ?? "fixture" }).responses(
|
||||
"openai/gpt-5.4-mini",
|
||||
),
|
||||
},
|
||||
{
|
||||
provider: "digitalocean",
|
||||
env: "DIGITAL_OCEAN_OFFICIAL_API_KEY",
|
||||
model: DigitalOcean.configure({ apiKey: process.env.DIGITAL_OCEAN_OFFICIAL_API_KEY ?? "fixture" }).responses(
|
||||
"openai-gpt-5.4-mini",
|
||||
),
|
||||
},
|
||||
{
|
||||
provider: "venice",
|
||||
env: "VENICE_API_KEY",
|
||||
model: Venice.configure({ apiKey: process.env.VENICE_API_KEY ?? "fixture" }).responses("openai-gpt-54-mini"),
|
||||
},
|
||||
]) {
|
||||
const recorded = recordedTests({
|
||||
prefix: `${item.provider}-responses`,
|
||||
provider: item.provider,
|
||||
protocol: "open-responses",
|
||||
requires: [item.env],
|
||||
metadata: { model: item.model.id },
|
||||
})
|
||||
|
||||
recorded.effect.with("streams text", { tags: ["text", "usage"] }, () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
LLM.request({ model: item.model, prompt: "Reply exactly: OK", generation: { maxTokens: 64 } }),
|
||||
)
|
||||
expect(response.text.trim()).toMatch(/^OK\.?$/)
|
||||
expect(response.events.some(LLMEvent.is.textDelta)).toBe(true)
|
||||
expect(response.finishReason.normalized).toBe("stop")
|
||||
expect(response.usage.outputTokens).toBeGreaterThan(0)
|
||||
}),
|
||||
)
|
||||
|
||||
recorded.effect.with("continues a tool call", { tags: ["tool", "tool-loop"] }, () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* runWeatherToolLoop(
|
||||
weatherToolLoopRequest({
|
||||
id: `${item.provider}-responses-tool-loop`,
|
||||
model: item.model,
|
||||
maxTokens: 256,
|
||||
temperature: false,
|
||||
}),
|
||||
)
|
||||
expectWeatherToolLoop(events)
|
||||
}),
|
||||
)
|
||||
|
||||
recorded.effect.with("calls a freeform text tool", { tags: ["tool", "freeform"] }, () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
LLM.request({
|
||||
model: item.model,
|
||||
prompt: "Use the run tool to execute exactly this code: print(1)",
|
||||
tools: run,
|
||||
toolChoice: "required",
|
||||
generation: { maxTokens: 256 },
|
||||
}),
|
||||
)
|
||||
expect(response.toolCalls).toMatchObject([{ name: "run", input: { code: expect.stringContaining("print(1)") } }])
|
||||
expect(response.toolCalls[0]?.providerMetadata).toMatchObject({ [item.provider]: { custom: true } })
|
||||
expect(response.finishReason.normalized).toBe("tool-calls")
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
@ -95,6 +95,7 @@ const builtins = new Map<string, () => Promise<unknown>>([
|
|||
["@opencode/ai/providers/digitalocean", () => import("@opencode/ai/providers/digitalocean")],
|
||||
["@opencode/ai/providers/digitalocean/decisions", () => import("@opencode/ai/providers/digitalocean/decisions")],
|
||||
["@opencode/ai/providers/digitalocean/images", () => import("@opencode/ai/providers/digitalocean/images")],
|
||||
["@opencode/ai/providers/digitalocean/responses", () => import("@opencode/ai/providers/digitalocean/responses")],
|
||||
["@opencode/ai/providers/fireworks", () => import("@opencode/ai/providers/fireworks")],
|
||||
["@opencode/ai/providers/google", () => import("@opencode/ai/providers/google")],
|
||||
["@opencode/ai/providers/google/interactions", () => import("@opencode/ai/providers/google/interactions")],
|
||||
|
|
@ -144,9 +145,11 @@ const builtins = new Map<string, () => Promise<unknown>>([
|
|||
["@opencode/ai/providers/openrouter", () => import("@opencode/ai/providers/openrouter")],
|
||||
["@opencode/ai/providers/openrouter/decisions", () => import("@opencode/ai/providers/openrouter/decisions")],
|
||||
["@opencode/ai/providers/openrouter/images", () => import("@opencode/ai/providers/openrouter/images")],
|
||||
["@opencode/ai/providers/openrouter/responses", () => import("@opencode/ai/providers/openrouter/responses")],
|
||||
["@opencode/ai/providers/togetherai", () => import("@opencode/ai/providers/togetherai")],
|
||||
["@opencode/ai/providers/typesafe-ai/decisions", () => import("@opencode/ai/providers/typesafe-ai/decisions")],
|
||||
["@opencode/ai/providers/venice", () => import("@opencode/ai/providers/venice")],
|
||||
["@opencode/ai/providers/venice/responses", () => import("@opencode/ai/providers/venice/responses")],
|
||||
["@opencode/ai/providers/vercel-ai-gateway", () => import("@opencode/ai/providers/vercel-ai-gateway")],
|
||||
["@opencode/ai/providers/vercel-ai-gateway/chat", () => import("@opencode/ai/providers/vercel-ai-gateway/chat")],
|
||||
[
|
||||
|
|
|
|||
|
|
@ -621,6 +621,9 @@ const PROTOCOLS: Readonly<Record<string, Protocol>> = {
|
|||
"@opencode/ai/providers/openai-compatible-responses": openResponses,
|
||||
"@opencode/ai/providers/google-vertex/responses": openResponses,
|
||||
"@opencode/ai/providers/xai": openResponses,
|
||||
"@opencode/ai/providers/digitalocean/responses": openResponses,
|
||||
"@opencode/ai/providers/openrouter/responses": openResponses,
|
||||
"@opencode/ai/providers/venice/responses": openResponses,
|
||||
|
||||
"@opencode/ai/providers/openai-compatible": openaiCompatible,
|
||||
"@opencode/ai/providers/openai/chat": openaiChat,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue