mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-26 19:31:39 -04:00
feat(ai): add native Cerebras and Together AI providers (#45098)
This commit is contained in:
@@ -346,7 +346,6 @@
|
||||
"@ai-sdk/amazon-bedrock": "4.0.112",
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/azure": "3.0.88",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/deepinfra": "2.0.41",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
@@ -357,7 +356,6 @@
|
||||
"@ai-sdk/perplexity": "3.0.26",
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@ai-sdk/provider-utils": "4.0.23",
|
||||
"@ai-sdk/togetherai": "2.0.41",
|
||||
"@ai-sdk/vercel": "2.0.39",
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@ff-labs/fff-bun": "0.10.5",
|
||||
@@ -1207,8 +1205,6 @@
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="],
|
||||
|
||||
"@ai-sdk/togetherai": ["@ai-sdk/togetherai@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-k3p9e3k0/gpDDyTtvafsK4HYR4D/aUQW/kzCwWo1+CzdBU84i4L14gWISC/mv6tgSicMXHcEUd521fPufQwNlg=="],
|
||||
|
||||
"@ai-sdk/vercel": ["@ai-sdk/vercel@2.0.39", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8eu3ljJpkCTP4ppcyYB+NcBrkcBoSOFthCSgk5VnjaxnDaOJFaxnPwfddM7wx3RwMk2CiK1O61Px/LlqNc7QkQ=="],
|
||||
|
||||
"@ai-sdk/xai": ["@ai-sdk/xai@3.0.123", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.69", "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-WNASvd1C516oh2qYIj9EvAVPdU+Abads8DQWU6p9lQtvFFeGh8QW+3LDOARZd1GCINUFfw5yadEK845SMQKLsA=="],
|
||||
@@ -5955,10 +5951,6 @@
|
||||
|
||||
"@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
|
||||
|
||||
"@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
|
||||
|
||||
"@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
@@ -157,9 +157,9 @@ const PROVIDERS: ReadonlyArray<Provider> = [
|
||||
id: "togetherai",
|
||||
label: "TogetherAI",
|
||||
tier: "compatible",
|
||||
note: "Existing OpenAI-compatible text/tool recorded tests",
|
||||
vars: [{ name: "TOGETHER_AI_API_KEY" }],
|
||||
validate: (env) => validateBearer("https://api.together.xyz/v1/models", Redacted.make(env.TOGETHER_AI_API_KEY)),
|
||||
note: "Native Together AI text/tool recorded tests",
|
||||
vars: [{ name: "TOGETHER_API_KEY" }],
|
||||
validate: (env) => validateBearer("https://api.together.xyz/v1/models", Redacted.make(env.TOGETHER_API_KEY)),
|
||||
},
|
||||
{
|
||||
id: "minimax",
|
||||
@@ -200,8 +200,8 @@ const PROVIDERS: ReadonlyArray<Provider> = [
|
||||
{
|
||||
id: "cerebras",
|
||||
label: "Cerebras",
|
||||
tier: "optional",
|
||||
note: "OpenAI-compatible bridge",
|
||||
tier: "compatible",
|
||||
note: "Native Cerebras text/tool/tool-loop recorded tests",
|
||||
vars: [{ name: "CEREBRAS_API_KEY" }],
|
||||
validate: (env) => validateBearer("https://api.cerebras.ai/v1/models", Redacted.make(env.CEREBRAS_API_KEY)),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { OpenAICompatibleChat } from "../protocols/openai-compatible-chat.js"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
import type { RouteDefaultsInput } from "../route/client.js"
|
||||
import { ProviderID, type ModelID } from "../schema/index.js"
|
||||
import { profiles } from "./openai-compatible-profile.js"
|
||||
import type { OpenAIProviderOptionsInput } from "./openai-options.js"
|
||||
|
||||
export const id = ProviderID.make("cerebras")
|
||||
|
||||
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export interface Settings extends ProviderPackage.Settings {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export const route = OpenAICompatibleChat.route.with({
|
||||
id: "cerebras-chat",
|
||||
provider: id,
|
||||
endpoint: { baseURL: profiles.cerebras.baseURL },
|
||||
})
|
||||
|
||||
export const routes = [route]
|
||||
|
||||
export const configure = (input: LanguageModelOptions = {}) => {
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL, ...defaults } = input
|
||||
const configured = route.with({
|
||||
...defaults,
|
||||
endpoint: { baseURL: baseURL ?? profiles.cerebras.baseURL },
|
||||
auth: AuthOptions.bearer(input, "CEREBRAS_API_KEY"),
|
||||
})
|
||||
return {
|
||||
id,
|
||||
model: (modelID: string | ModelID) =>
|
||||
configured.model<OpenAIProviderOptionsInput>({
|
||||
id: modelID,
|
||||
compatibility: { maxTokensField: "max_tokens", reasoningField: "reasoning", supportsStore: false },
|
||||
}),
|
||||
configure,
|
||||
}
|
||||
}
|
||||
|
||||
export const provider = configure()
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
|
||||
configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers === undefined ? undefined : { ...settings.headers },
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
}).model(modelID)
|
||||
@@ -3,6 +3,7 @@ export * as AnthropicCompatible from "./anthropic-compatible.js"
|
||||
export * as AmazonBedrock from "./amazon-bedrock.js"
|
||||
export * as AmazonBedrockMantle from "./amazon-bedrock-mantle.js"
|
||||
export * as Azure from "./azure.js"
|
||||
export * as Cerebras from "./cerebras.js"
|
||||
export * as Cloudflare from "./cloudflare.js"
|
||||
export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare.js"
|
||||
export * as Google from "./google.js"
|
||||
@@ -14,5 +15,6 @@ export * as OpenAI from "./openai.js"
|
||||
export * as OpenAICompatible from "./openai-compatible.js"
|
||||
export * as OpenAICompatibleResponses from "./openai-compatible-responses.js"
|
||||
export * as OpenRouter from "./openrouter.js"
|
||||
export * as TogetherAI from "./togetherai.js"
|
||||
export * as XAI from "./xai.js"
|
||||
export * as ZAI from "./zai.js"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { OpenAICompatibleChat } from "../protocols/openai-compatible-chat.js"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
import type { RouteDefaultsInput } from "../route/client.js"
|
||||
import { ProviderID, type ModelID } from "../schema/index.js"
|
||||
import { profiles } from "./openai-compatible-profile.js"
|
||||
import type { OpenAIProviderOptionsInput } from "./openai-options.js"
|
||||
|
||||
export const id = ProviderID.make("togetherai")
|
||||
|
||||
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export interface Settings extends ProviderPackage.Settings {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export const route = OpenAICompatibleChat.route.with({
|
||||
id: "togetherai-chat",
|
||||
provider: id,
|
||||
endpoint: { baseURL: profiles.togetherai.baseURL },
|
||||
})
|
||||
|
||||
export const routes = [route]
|
||||
|
||||
export const configure = (input: LanguageModelOptions = {}) => {
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL, ...defaults } = input
|
||||
const configured = route.with({
|
||||
...defaults,
|
||||
endpoint: { baseURL: baseURL ?? profiles.togetherai.baseURL },
|
||||
auth: AuthOptions.bearer(input, ["TOGETHER_API_KEY", "TOGETHER_AI_API_KEY"]),
|
||||
})
|
||||
return {
|
||||
id,
|
||||
model: (modelID: string | ModelID) =>
|
||||
configured.model<OpenAIProviderOptionsInput>({
|
||||
id: modelID,
|
||||
compatibility: { maxTokensField: "max_tokens", supportsStore: false, supportsStrictMode: false },
|
||||
}),
|
||||
configure,
|
||||
}
|
||||
}
|
||||
|
||||
export const provider = configure()
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
|
||||
configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers === undefined ? undefined : { ...settings.headers },
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
}).model(modelID)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"provider": "cerebras",
|
||||
"route": "cerebras-chat",
|
||||
"transport": "http",
|
||||
"model": "gpt-oss-120b",
|
||||
"tags": ["prefix:cerebras-chat", "provider:cerebras", "text", "golden"],
|
||||
"name": "cerebras-chat/cerebras-gpt-oss-120b-text",
|
||||
"recordedAt": "2026-08-25T23:55:27.619Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.cerebras.ai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-oss-120b\",\"messages\":[{\"role\":\"system\",\"content\":\"You are concise.\"},{\"role\":\"user\",\"content\":\"Reply exactly with: Hello!\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":256}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-cd45cd6e-886a-433e-8ba9-caca78c1f13a\",\"choices\":[{\"delta\":{\"role\":\"assistant\"},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_e2cabf4999eb0aead3d1\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cd45cd6e-886a-433e-8ba9-caca78c1f13a\",\"choices\":[{\"delta\":{\"reasoning\":\"The\"},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_e2cabf4999eb0aead3d1\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cd45cd6e-886a-433e-8ba9-caca78c1f13a\",\"choices\":[{\"delta\":{\"reasoning\":\" user says: \\\"Reply exactly with: Hello!\\\" So we must output exactly\"},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_e2cabf4999eb0aead3d1\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cd45cd6e-886a-433e-8ba9-caca78c1f13a\",\"choices\":[{\"delta\":{\"reasoning\":\" \\\"Hello!\\\" with no extra characters, no formatting\"},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_e2cabf4999eb0aead3d1\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cd45cd6e-886a-433e-8ba9-caca78c1f13a\",\"choices\":[{\"delta\":{\"reasoning\":\". Ensure no extra spaces or new\"},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_e2cabf4999eb0aead3d1\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cd45cd6e-886a-433e-8ba9-caca78c1f13a\",\"choices\":[{\"delta\":{\"reasoning\":\"lines? Probably just \\\"Hello!\\\".\"},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_e2cabf4999eb0aead3d1\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cd45cd6e-886a-433e-8ba9-caca78c1f13a\",\"choices\":[{\"delta\":{\"reasoning\":\" Usually we output exactly that.\"},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_e2cabf4999eb0aead3d1\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cd45cd6e-886a-433e-8ba9-caca78c1f13a\",\"choices\":[{\"delta\":{\"content\":\"Hello!\"},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_e2cabf4999eb0aead3d1\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cd45cd6e-886a-433e-8ba9-caca78c1f13a\",\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\",\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_e2cabf4999eb0aead3d1\",\"object\":\"chat.completion.chunk\",\"usage\":{\"total_tokens\":142,\"completion_tokens\":58,\"completion_tokens_details\":{\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0,\"reasoning_tokens\":46},\"prompt_tokens\":84,\"prompt_tokens_details\":{\"cached_tokens\":0}},\"time_info\":{\"created\":1787702127.645281,\"queue_time\":0.003817115,\"prompt_time\":0.001587193,\"completion_time\":0.029805929,\"total_time\":0.036823272705078125}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"provider": "cerebras",
|
||||
"route": "cerebras-chat",
|
||||
"transport": "http",
|
||||
"model": "gpt-oss-120b",
|
||||
"tags": ["prefix:cerebras-chat", "provider:cerebras", "tool", "tool-call", "golden"],
|
||||
"name": "cerebras-chat/cerebras-gpt-oss-120b-tool-call",
|
||||
"recordedAt": "2026-08-25T23:55:28.454Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.cerebras.ai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-oss-120b\",\"messages\":[{\"role\":\"system\",\"content\":\"Call tools exactly as requested.\"},{\"role\":\"user\",\"content\":\"Call get_weather with city exactly Paris.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"get_weather\"}},\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":512}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-402a09cc-8668-446d-8f9c-3e4121f5fa51\",\"choices\":[{\"delta\":{\"role\":\"assistant\"},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-402a09cc-8668-446d-8f9c-3e4121f5fa51\",\"choices\":[{\"delta\":{\"reasoning\":\"We\"},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-402a09cc-8668-446d-8f9c-3e4121f5fa51\",\"choices\":[{\"delta\":{\"reasoning\":\" need to call the function get_weather with city \\\"\"},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-402a09cc-8668-446d-8f9c-3e4121f5fa51\",\"choices\":[{\"delta\":{\"reasoning\":\"Paris\\\".\"},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-402a09cc-8668-446d-8f9c-3e4121f5fa51\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"},\"type\":\"function\",\"id\":\"3d860cefe\",\"index\":0}]},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-402a09cc-8668-446d-8f9c-3e4121f5fa51\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"function\":{\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},\"type\":\"function\",\"index\":0}]},\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-402a09cc-8668-446d-8f9c-3e4121f5fa51\",\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\",\"index\":0}],\"created\":1787702127,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\",\"usage\":{\"total_tokens\":174,\"completion_tokens\":37,\"completion_tokens_details\":{\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0,\"reasoning_tokens\":13},\"prompt_tokens\":137,\"prompt_tokens_details\":{\"cached_tokens\":0}},\"time_info\":{\"created\":1787702127.8019717,\"queue_time\":0.31196235,\"prompt_time\":0.005234764,\"completion_time\":0.020198402,\"total_time\":0.702225923538208}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+50
File diff suppressed because one or more lines are too long
@@ -26,6 +26,8 @@ describe("provider package entrypoints", () => {
|
||||
import("@opencode-ai/ai/providers/amazon-bedrock/mantle"),
|
||||
import("@opencode-ai/ai/providers/amazon-bedrock/mantle/chat"),
|
||||
import("@opencode-ai/ai/providers/amazon-bedrock/mantle/responses"),
|
||||
import("@opencode-ai/ai/providers/togetherai"),
|
||||
import("@opencode-ai/ai/providers/cerebras"),
|
||||
])
|
||||
|
||||
for (const module of modules) expect(module.model).toBeFunction()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Anthropic from "../../src/providers/anthropic.js"
|
||||
import * as AnthropicCompatible from "../../src/providers/anthropic-compatible.js"
|
||||
import { Cerebras, TogetherAI } from "../../src/providers/index.js"
|
||||
import { CloudflareAIGateway, CloudflareWorkersAI } from "../../src/providers/cloudflare.js"
|
||||
import * as Google from "../../src/providers/google.js"
|
||||
import * as OpenAI from "../../src/providers/openai.js"
|
||||
@@ -47,11 +48,10 @@ const cloudflareWorkersAITools = cloudflareWorkers.model("@cf/openai/gpt-oss-20b
|
||||
const deepseek = OpenAICompatible.deepseek
|
||||
.configure({ apiKey: process.env.DEEPSEEK_API_KEY ?? "fixture" })
|
||||
.model("deepseek-chat")
|
||||
const together = OpenAICompatible.togetherai
|
||||
.configure({
|
||||
apiKey: process.env.TOGETHER_AI_API_KEY ?? "fixture",
|
||||
})
|
||||
.model("meta-llama/Llama-3.3-70B-Instruct-Turbo")
|
||||
const together = TogetherAI.configure({
|
||||
apiKey: process.env.TOGETHER_API_KEY ?? process.env.TOGETHER_AI_API_KEY ?? "fixture",
|
||||
}).model("meta-llama/Llama-3.3-70B-Instruct-Turbo")
|
||||
const cerebras = Cerebras.configure({ apiKey: process.env.CEREBRAS_API_KEY ?? "fixture" }).model("gpt-oss-120b")
|
||||
const groq = OpenAICompatible.groq
|
||||
.configure({ apiKey: process.env.GROQ_API_KEY ?? "fixture" })
|
||||
.model("llama-3.3-70b-versatile")
|
||||
@@ -193,8 +193,27 @@ describeRecordedGoldenScenarios([
|
||||
name: "TogetherAI Llama 3.3 70B",
|
||||
prefix: "openai-compatible-chat",
|
||||
model: together,
|
||||
requires: ["TOGETHER_AI_API_KEY"],
|
||||
scenarios: ["text", "tool-call"],
|
||||
requires: ["TOGETHER_API_KEY"],
|
||||
scenarios: [
|
||||
{
|
||||
id: "text",
|
||||
cassette: "openai-compatible-chat/togetherai-streams-text",
|
||||
prompt: "Reply with exactly: Hello!",
|
||||
maxTokens: 20,
|
||||
},
|
||||
{ id: "tool-call", cassette: "openai-compatible-chat/togetherai-streams-tool-call" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Cerebras GPT OSS 120B",
|
||||
prefix: "cerebras-chat",
|
||||
model: cerebras,
|
||||
requires: ["CEREBRAS_API_KEY"],
|
||||
scenarios: [
|
||||
{ id: "text", maxTokens: 256, temperature: false },
|
||||
{ id: "tool-call", maxTokens: 512, temperature: false },
|
||||
{ id: "tool-loop", maxTokens: 512, temperature: false, timeout: 30_000 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Groq Llama 3.3 70B",
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM, Message, ToolDefinition } from "../../src/index.js"
|
||||
import { Cerebras, TogetherAI } from "../../src/providers/index.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
import { dynamicResponse } from "../lib/http.js"
|
||||
import { sseEvents } from "../lib/sse.js"
|
||||
|
||||
describe("native OpenAI-compatible providers", () => {
|
||||
it.effect("preserves native Together AI and Cerebras provider and route identities", () =>
|
||||
Effect.gen(function* () {
|
||||
const together = TogetherAI.configure({ apiKey: "fixture" }).model("meta-llama/Llama-3.3-70B")
|
||||
const cerebras = Cerebras.configure({ apiKey: "fixture" }).model("qwen-3-235b-a22b")
|
||||
|
||||
expect(together).toMatchObject({
|
||||
provider: "togetherai",
|
||||
compatibility: { maxTokensField: "max_tokens", supportsStore: false, supportsStrictMode: false },
|
||||
route: { id: "togetherai-chat", protocol: "openai-chat" },
|
||||
})
|
||||
expect(together.route.endpoint.baseURL).toBe("https://api.together.xyz/v1")
|
||||
expect(cerebras).toMatchObject({
|
||||
provider: "cerebras",
|
||||
compatibility: { maxTokensField: "max_tokens", reasoningField: "reasoning", supportsStore: false },
|
||||
route: { id: "cerebras-chat", protocol: "openai-chat" },
|
||||
})
|
||||
expect(cerebras.route.endpoint.baseURL).toBe("https://api.cerebras.ai/v1")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("applies native provider request defaults even with a custom gateway URL", () =>
|
||||
Effect.gen(function* () {
|
||||
const together = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: TogetherAI.configure({ apiKey: "fixture", baseURL: "https://gateway.example/v1" }).model("llama"),
|
||||
prompt: "Use a tool.",
|
||||
generation: { maxTokens: 32 },
|
||||
tools: [
|
||||
ToolDefinition.make({ name: "lookup", description: "Look up data", inputSchema: { type: "object" } }),
|
||||
],
|
||||
providerOptions: { store: true },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(together.body).toMatchObject({
|
||||
max_tokens: 32,
|
||||
stream_options: { include_usage: true },
|
||||
tools: [{ function: { name: "lookup" } }],
|
||||
})
|
||||
expect(together.body).not.toHaveProperty("max_completion_tokens")
|
||||
expect(together.body).not.toHaveProperty("store")
|
||||
expect(together.body.tools?.[0]?.function).not.toHaveProperty("strict")
|
||||
|
||||
const cerebras = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: Cerebras.configure({ apiKey: "fixture", baseURL: "https://gateway.example/v1" }).model("qwen"),
|
||||
generation: { maxTokens: 48 },
|
||||
messages: [
|
||||
Message.user("Think first."),
|
||||
Message.assistant([
|
||||
{ type: "reasoning", text: "A deliberate thought." },
|
||||
{ type: "text", text: "An answer." },
|
||||
]),
|
||||
Message.user("Continue."),
|
||||
],
|
||||
providerOptions: { store: true },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(cerebras.body).toMatchObject({
|
||||
max_tokens: 48,
|
||||
messages: [
|
||||
{ role: "user", content: "Think first." },
|
||||
{ role: "assistant", content: "An answer.", reasoning: "A deliberate thought." },
|
||||
{ role: "user", content: "Continue." },
|
||||
],
|
||||
})
|
||||
expect(cerebras.body).not.toHaveProperty("max_completion_tokens")
|
||||
expect(cerebras.body).not.toHaveProperty("store")
|
||||
expect(cerebras.body.messages[1]).not.toHaveProperty("reasoning_content")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("maps package settings onto native executable models", () =>
|
||||
Effect.gen(function* () {
|
||||
for (const native of [TogetherAI, Cerebras]) {
|
||||
const selected = native.model("provider-model", {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://gateway.example/v1",
|
||||
headers: { "x-application": "opencode" },
|
||||
body: { service_tier: "priority" },
|
||||
providerOptions: { reasoningEffort: "high" },
|
||||
})
|
||||
|
||||
expect(selected.route.endpoint.baseURL).toBe("https://gateway.example/v1")
|
||||
expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" })
|
||||
expect(selected.route.defaults.http?.body).toEqual({ service_tier: "priority" })
|
||||
expect(selected.route.defaults.providerOptions).toEqual({ reasoningEffort: "high" })
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("resolves provider environment credentials and preserves deprecated Together credentials", () =>
|
||||
Effect.gen(function* () {
|
||||
const scenarios = [
|
||||
{
|
||||
model: TogetherAI.configure().model("llama"),
|
||||
env: { TOGETHER_API_KEY: "together-primary", TOGETHER_AI_API_KEY: "together-legacy" },
|
||||
token: "together-primary",
|
||||
url: "https://api.together.xyz/v1/chat/completions",
|
||||
},
|
||||
{
|
||||
model: TogetherAI.configure().model("llama"),
|
||||
env: { TOGETHER_AI_API_KEY: "together-legacy" },
|
||||
token: "together-legacy",
|
||||
url: "https://api.together.xyz/v1/chat/completions",
|
||||
},
|
||||
{
|
||||
model: Cerebras.configure().model("qwen"),
|
||||
env: { CEREBRAS_API_KEY: "cerebras-secret" },
|
||||
token: "cerebras-secret",
|
||||
url: "https://api.cerebras.ai/v1/chat/completions",
|
||||
},
|
||||
]
|
||||
|
||||
yield* Effect.forEach(scenarios, (scenario) =>
|
||||
LLM.generate(LLM.request({ model: scenario.model, prompt: "Say hello." })).pipe(
|
||||
Effect.provide(
|
||||
dynamicResponse((input) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
||||
expect(request.url).toBe(scenario.url)
|
||||
expect(request.headers.get("authorization")).toBe(`Bearer ${scenario.token}`)
|
||||
return input.respond(
|
||||
sseEvents(
|
||||
{ id: "chatcmpl_fixture", choices: [{ delta: { content: "Hello" }, finish_reason: null }] },
|
||||
{ id: "chatcmpl_fixture", choices: [{ delta: {}, finish_reason: "stop" }] },
|
||||
),
|
||||
{ headers: { "content-type": "text/event-stream" } },
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: scenario.env }))),
|
||||
Effect.tap((response) => Effect.sync(() => expect(response.text).toBe("Hello"))),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -20,6 +20,7 @@ type ScenarioInput =
|
||||
readonly name?: string
|
||||
readonly cassette?: string
|
||||
readonly tags?: ReadonlyArray<string>
|
||||
readonly prompt?: string
|
||||
readonly maxTokens?: number
|
||||
readonly temperature?: number | false
|
||||
readonly timeout?: number
|
||||
@@ -87,6 +88,7 @@ const runTarget = (target: TargetInput) => {
|
||||
yield* runGoldenScenario(input.id, {
|
||||
id: `recorded_${kebab(target.name).replaceAll("-", "_")}_${input.id.replaceAll("-", "_")}`,
|
||||
model: target.model,
|
||||
prompt: input.prompt,
|
||||
maxTokens: input.maxTokens,
|
||||
temperature: input.temperature,
|
||||
})
|
||||
|
||||
@@ -164,6 +164,7 @@ export const expectGoldenWeatherToolLoop = (events: ReadonlyArray<LLMEvent>) =>
|
||||
export interface GoldenScenarioContext {
|
||||
readonly id: string
|
||||
readonly model: LanguageModel
|
||||
readonly prompt?: string
|
||||
readonly maxTokens?: number
|
||||
readonly temperature?: number | false
|
||||
}
|
||||
@@ -298,7 +299,7 @@ const runGeneratedConversation = (context: GoldenScenarioContext, steps: Readonl
|
||||
|
||||
const runTextScenario = (context: GoldenScenarioContext) =>
|
||||
runGeneratedConversation(context, [
|
||||
user("Reply exactly with: Hello!"),
|
||||
user(context.prompt ?? "Reply exactly with: Hello!"),
|
||||
assistant.expectText(/^Hello!?$/, {
|
||||
system: "You are concise.",
|
||||
maxTokens: context.maxTokens ?? 40,
|
||||
|
||||
@@ -102,7 +102,6 @@
|
||||
"@ai-sdk/amazon-bedrock": "4.0.112",
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/azure": "3.0.88",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/deepinfra": "2.0.41",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
@@ -113,7 +112,6 @@
|
||||
"@ai-sdk/perplexity": "3.0.26",
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@ai-sdk/provider-utils": "4.0.23",
|
||||
"@ai-sdk/togetherai": "2.0.41",
|
||||
"@ai-sdk/vercel": "2.0.39",
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@lydell/node-pty": "catalog:",
|
||||
|
||||
@@ -53,6 +53,17 @@ export function map(input: MapInput): Mapping | undefined {
|
||||
...mapOpenAIOptions(input.settings),
|
||||
},
|
||||
}
|
||||
case "@ai-sdk/cerebras":
|
||||
case "@ai-sdk/togetherai":
|
||||
return {
|
||||
package: `@opencode-ai/ai/providers/${input.packageName === "@ai-sdk/cerebras" ? "cerebras" : "togetherai"}`,
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(input.settings),
|
||||
...mapProviderOptions(input.settings, ["apiKey", "baseURL", "fetch", "headers", "name"]),
|
||||
},
|
||||
...(isStringRecord(input.settings.headers) ? { headers: input.settings.headers } : {}),
|
||||
}
|
||||
case "@ai-sdk/google":
|
||||
return {
|
||||
package: "@opencode-ai/ai/providers/google",
|
||||
|
||||
@@ -333,8 +333,10 @@ function usesAPIKeyAuth(packageName: string | undefined) {
|
||||
return (
|
||||
name === "@ai-sdk/openai" ||
|
||||
name === "@ai-sdk/anthropic" ||
|
||||
name === "@ai-sdk/cerebras" ||
|
||||
name === "@ai-sdk/openai-compatible" ||
|
||||
name === "@ai-sdk/google" ||
|
||||
name === "@ai-sdk/togetherai" ||
|
||||
name === "@ai-sdk/xai" ||
|
||||
name === "@openrouter/ai-sdk-provider" ||
|
||||
name === "@ai-sdk/azure" ||
|
||||
@@ -342,8 +344,10 @@ function usesAPIKeyAuth(packageName: string | undefined) {
|
||||
name?.startsWith("@opencode-ai/ai/providers/openai/") === true ||
|
||||
name === "@opencode-ai/ai/providers/anthropic" ||
|
||||
name === "@opencode-ai/ai/providers/anthropic-compatible" ||
|
||||
name === "@opencode-ai/ai/providers/cerebras" ||
|
||||
name === "@opencode-ai/ai/providers/openai-compatible" ||
|
||||
name === "@opencode-ai/ai/providers/google" ||
|
||||
name === "@opencode-ai/ai/providers/togetherai" ||
|
||||
name === "@opencode-ai/ai/providers/xai" ||
|
||||
name === "@opencode-ai/ai/providers/openrouter" ||
|
||||
name === "@opencode-ai/ai/providers/azure" ||
|
||||
|
||||
@@ -26,7 +26,6 @@ import { OpencodePlugin } from "./provider/opencode.js"
|
||||
import { OpenRouterPlugin } from "./provider/openrouter.js"
|
||||
import { PerplexityPlugin } from "./provider/perplexity.js"
|
||||
import { SapAICorePlugin } from "./provider/sap-ai-core.js"
|
||||
import { TogetherAIPlugin } from "./provider/togetherai.js"
|
||||
import { VercelPlugin } from "./provider/vercel.js"
|
||||
import { VenicePlugin } from "./provider/venice.js"
|
||||
import { VLLMPlugin } from "./provider/vllm.js"
|
||||
@@ -62,7 +61,6 @@ export const ProviderPlugins: PluginInternal.InternalPlugin[] = [
|
||||
OpenRouterPlugin,
|
||||
PerplexityPlugin,
|
||||
SapAICorePlugin,
|
||||
TogetherAIPlugin,
|
||||
VercelPlugin,
|
||||
VenicePlugin,
|
||||
VLLMPlugin,
|
||||
|
||||
@@ -7,20 +7,12 @@ export const CerebrasPlugin = define({
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.catalog.transform((evt) => {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!Provider.isAISDK(item.provider.package)) continue
|
||||
if (Provider.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue
|
||||
const name = Provider.packageName(item.provider.package)
|
||||
if (name !== "@ai-sdk/cerebras" && name !== "@opencode-ai/ai/providers/cerebras") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" }
|
||||
})
|
||||
}
|
||||
})
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/cerebras") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
|
||||
evt.sdk = mod.createCerebras(evt.options)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { createProviderPlugin } from "./factory.js"
|
||||
|
||||
export const TogetherAIPlugin = createProviderPlugin({
|
||||
id: "opencode.provider.togetherai",
|
||||
package: "@ai-sdk/togetherai",
|
||||
load: async (options) => {
|
||||
const { createTogetherAI } = await import("@ai-sdk/togetherai")
|
||||
return createTogetherAI(options)
|
||||
},
|
||||
})
|
||||
@@ -48,6 +48,7 @@ const builtins = new Map<string, () => Promise<unknown>>([
|
||||
["@opencode-ai/ai/providers/azure", () => import("@opencode-ai/ai/providers/azure")],
|
||||
["@opencode-ai/ai/providers/azure/chat", () => import("@opencode-ai/ai/providers/azure/chat")],
|
||||
["@opencode-ai/ai/providers/azure/responses", () => import("@opencode-ai/ai/providers/azure/responses")],
|
||||
["@opencode-ai/ai/providers/cerebras", () => import("@opencode-ai/ai/providers/cerebras")],
|
||||
["@opencode-ai/ai/providers/google", () => import("@opencode-ai/ai/providers/google")],
|
||||
["@opencode-ai/ai/providers/google-vertex", () => import("@opencode-ai/ai/providers/google-vertex")],
|
||||
["@opencode-ai/ai/providers/google-vertex/gemini", () => import("@opencode-ai/ai/providers/google-vertex/gemini")],
|
||||
@@ -65,6 +66,7 @@ const builtins = new Map<string, () => Promise<unknown>>([
|
||||
["@opencode-ai/ai/providers/openai/responses", () => import("@opencode-ai/ai/providers/openai/responses")],
|
||||
["@opencode-ai/ai/providers/openai-compatible", () => import("@opencode-ai/ai/providers/openai-compatible")],
|
||||
["@opencode-ai/ai/providers/openrouter", () => import("@opencode-ai/ai/providers/openrouter")],
|
||||
["@opencode-ai/ai/providers/togetherai", () => import("@opencode-ai/ai/providers/togetherai")],
|
||||
["@opencode-ai/ai/providers/xai", () => import("@opencode-ai/ai/providers/xai")],
|
||||
])
|
||||
|
||||
|
||||
@@ -61,6 +61,33 @@ describe("AISDKNative", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("maps Cerebras and Together AI settings, headers, and reasoning options to native providers", () => {
|
||||
for (const name of ["cerebras", "togetherai"]) {
|
||||
expect(
|
||||
map(`@ai-sdk/${name}`, {
|
||||
apiKey: "secret",
|
||||
baseURL: `https://${name}.example/v1`,
|
||||
headers: { "x-provider": name },
|
||||
name: "custom-provider",
|
||||
reasoningEffort: "high",
|
||||
customOption: { enabled: true },
|
||||
}),
|
||||
).toEqual({
|
||||
package: `@opencode-ai/ai/providers/${name}`,
|
||||
settings: {
|
||||
apiKey: "secret",
|
||||
baseURL: `https://${name}.example/v1`,
|
||||
providerOptions: { reasoningEffort: "high", customOption: { enabled: true } },
|
||||
},
|
||||
headers: { "x-provider": name },
|
||||
})
|
||||
expect(map(`@ai-sdk/${name}`, {})).toEqual({
|
||||
package: `@opencode-ai/ai/providers/${name}`,
|
||||
settings: {},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test("maps Google Vertex settings to the native provider", () => {
|
||||
expect(
|
||||
map("@ai-sdk/google-vertex", {
|
||||
|
||||
@@ -879,6 +879,12 @@ describe("ModelResolver", () => {
|
||||
{ thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
||||
{ thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
||||
],
|
||||
[
|
||||
"@ai-sdk/cerebras",
|
||||
"@opencode-ai/ai/providers/cerebras",
|
||||
{ reasoningEffort: "high" },
|
||||
{ reasoningEffort: "high" },
|
||||
],
|
||||
[
|
||||
"@ai-sdk/openai-compatible",
|
||||
"@opencode-ai/ai/providers/openai-compatible",
|
||||
@@ -903,6 +909,12 @@ describe("ModelResolver", () => {
|
||||
{ reasoning: { effort: "high" } },
|
||||
{ reasoning: { effort: "high" } },
|
||||
],
|
||||
[
|
||||
"@ai-sdk/togetherai",
|
||||
"@opencode-ai/ai/providers/togetherai",
|
||||
{ reasoningEffort: "high" },
|
||||
{ reasoningEffort: "high" },
|
||||
],
|
||||
["@ai-sdk/xai", "@opencode-ai/ai/providers/xai", { reasoningEffort: "high" }, { reasoningEffort: "high" }],
|
||||
] as const
|
||||
|
||||
@@ -950,12 +962,14 @@ describe("ModelResolver", () => {
|
||||
"openai.gpt-oss-120b",
|
||||
],
|
||||
["@ai-sdk/azure", "@opencode-ai/ai/providers/azure/responses", "api-model"],
|
||||
["@ai-sdk/cerebras", "@opencode-ai/ai/providers/cerebras", "api-model"],
|
||||
["@ai-sdk/google", "@opencode-ai/ai/providers/google", "api-model"],
|
||||
["@ai-sdk/google-vertex", "@opencode-ai/ai/providers/google-vertex", "api-model"],
|
||||
["@ai-sdk/google-vertex/anthropic", "@opencode-ai/ai/providers/google-vertex/messages", "claude-sonnet-4-6"],
|
||||
["@ai-sdk/openai", "@opencode-ai/ai/providers/openai", "api-model"],
|
||||
["@ai-sdk/openai-compatible", "@opencode-ai/ai/providers/openai-compatible", "api-model"],
|
||||
["@openrouter/ai-sdk-provider", "@opencode-ai/ai/providers/openrouter", "api-model"],
|
||||
["@ai-sdk/togetherai", "@opencode-ai/ai/providers/togetherai", "api-model"],
|
||||
["@ai-sdk/xai", "@opencode-ai/ai/providers/xai", "api-model"],
|
||||
] as const
|
||||
|
||||
@@ -1070,6 +1084,12 @@ describe("ModelResolver", () => {
|
||||
settings: { reasoning: { effort: "high" } },
|
||||
}),
|
||||
)
|
||||
const cerebras = yield* ModelResolver.fromCatalogModel(
|
||||
model(Provider.aisdk("@ai-sdk/cerebras"), { settings: { reasoningEffort: "high" } }),
|
||||
)
|
||||
const togetherai = yield* ModelResolver.fromCatalogModel(
|
||||
model(Provider.aisdk("@ai-sdk/togetherai"), { settings: { reasoningEffort: "high" } }),
|
||||
)
|
||||
const xai = yield* ModelResolver.fromCatalogModel(
|
||||
model(Provider.aisdk("@ai-sdk/xai"), { settings: { reasoningEffort: "high" } }),
|
||||
)
|
||||
@@ -1090,6 +1110,12 @@ describe("ModelResolver", () => {
|
||||
expect(google.route.defaults.providerOptions).toEqual({ thinkingConfig: { thinkingBudget: 1_024 } })
|
||||
expect(openrouter.route.id).toBe("openrouter")
|
||||
expect(openrouter.route.defaults.providerOptions).toEqual({ reasoning: { effort: "high" } })
|
||||
expect(cerebras.route.id).toBe("cerebras-chat")
|
||||
expect(cerebras.route.defaults.providerOptions).toEqual({ reasoningEffort: "high" })
|
||||
expect(String(cerebras.provider)).toBe("test-provider")
|
||||
expect(togetherai.route.id).toBe("togetherai-chat")
|
||||
expect(togetherai.route.defaults.providerOptions).toEqual({ reasoningEffort: "high" })
|
||||
expect(String(togetherai.provider)).toBe("test-provider")
|
||||
expect(xai.route.id).toBe("openai-responses")
|
||||
expect(xai.route.defaults.providerOptions).toEqual({
|
||||
reasoningEffort: "high",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect, mock } from "bun:test"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Model } from "@opencode-ai/core/model"
|
||||
import { Plugin } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras"
|
||||
@@ -10,26 +8,14 @@ import { Provider } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const cerebrasOptions: Record<string, unknown>[] = []
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* CerebrasPlugin.effect(host)
|
||||
})
|
||||
|
||||
void mock.module("@ai-sdk/cerebras", () => ({
|
||||
createCerebras: (options: Record<string, unknown>) => {
|
||||
const snapshot = { ...options }
|
||||
cerebrasOptions.push(snapshot)
|
||||
return {
|
||||
languageModel: (modelID: string) => ({ modelID, provider: snapshot.name, specificationVersion: "v3" }),
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
describe("CerebrasPlugin", () => {
|
||||
it.effect("applies the legacy integration header", () =>
|
||||
Effect.gen(function* () {
|
||||
@@ -57,62 +43,21 @@ describe("CerebrasPlugin", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("creates a bundled Cerebras SDK with the model provider ID as the SDK name", () =>
|
||||
it.effect("applies the integration header to custom native Cerebras providers", () =>
|
||||
Effect.gen(function* () {
|
||||
cerebrasOptions.length = 0
|
||||
const plugin = yield* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* aisdk.runSDK({
|
||||
model: Model.Info.make({
|
||||
...Model.Info.default(Provider.ID.make("custom-cerebras"), Model.ID.make("llama-4-scout-17b-16e-instruct")),
|
||||
modelID: Model.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||
package: "aisdk:test-provider",
|
||||
}),
|
||||
package: "@ai-sdk/cerebras",
|
||||
options: { name: "custom-cerebras", apiKey: "test" },
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = Provider.ID.make("custom-cerebras")
|
||||
yield* catalog.transform((catalog) => {
|
||||
catalog.provider.update(providerID, (item) => {
|
||||
item.package = "@opencode-ai/ai/providers/cerebras"
|
||||
item.headers = { Existing: "1" }
|
||||
})
|
||||
})
|
||||
expect(cerebrasOptions).toEqual([{ name: "custom-cerebras", apiKey: "test" }])
|
||||
expect(result.sdk.languageModel("llama-4-scout-17b-16e-instruct").provider).toBe("custom-cerebras")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves an explicit bundled Cerebras SDK name option", () =>
|
||||
Effect.gen(function* () {
|
||||
cerebrasOptions.length = 0
|
||||
const plugin = yield* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
yield* aisdk.runSDK({
|
||||
model: Model.Info.make({
|
||||
...Model.Info.default(Provider.ID.make("custom-cerebras"), Model.ID.make("llama-4-scout-17b-16e-instruct")),
|
||||
modelID: Model.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||
package: "aisdk:test-provider",
|
||||
}),
|
||||
package: "@ai-sdk/cerebras",
|
||||
options: { name: "configured-cerebras", apiKey: "test" },
|
||||
expect((yield* catalog.provider.get(providerID))?.headers).toEqual({
|
||||
Existing: "1",
|
||||
"X-Cerebras-3rd-Party-Integration": "opencode",
|
||||
})
|
||||
expect(cerebrasOptions).toEqual([{ name: "configured-cerebras", apiKey: "test" }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("ignores non-Cerebras SDK packages", () =>
|
||||
Effect.gen(function* () {
|
||||
cerebrasOptions.length = 0
|
||||
const plugin = yield* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* aisdk.runSDK({
|
||||
model: Model.Info.make({
|
||||
...Model.Info.default(Provider.ID.make("custom-cerebras"), Model.ID.make("llama-4-scout-17b-16e-instruct")),
|
||||
modelID: Model.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||
package: "aisdk:test-provider",
|
||||
}),
|
||||
package: "@ai-sdk/groq",
|
||||
options: { name: "custom-cerebras", apiKey: "test" },
|
||||
})
|
||||
expect(cerebrasOptions).toEqual([])
|
||||
expect(result.sdk).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -11,7 +11,6 @@ import { GatewayPlugin } from "@opencode-ai/core/plugin/provider/gateway"
|
||||
import { GroqPlugin } from "@opencode-ai/core/plugin/provider/groq"
|
||||
import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral"
|
||||
import { PerplexityPlugin } from "@opencode-ai/core/plugin/provider/perplexity"
|
||||
import { TogetherAIPlugin } from "@opencode-ai/core/plugin/provider/togetherai"
|
||||
import { VenicePlugin } from "@opencode-ai/core/plugin/provider/venice"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
@@ -27,7 +26,6 @@ const providers = [
|
||||
{ id: "groq", plugin: GroqPlugin, package: "@ai-sdk/groq", provider: "groq.chat" },
|
||||
{ id: "mistral", plugin: MistralPlugin, package: "@ai-sdk/mistral", provider: "mistral.chat" },
|
||||
{ id: "perplexity", plugin: PerplexityPlugin, package: "@ai-sdk/perplexity", provider: "perplexity" },
|
||||
{ id: "togetherai", plugin: TogetherAIPlugin, package: "@ai-sdk/togetherai", provider: "togetherai.chat" },
|
||||
{ id: "venice", plugin: VenicePlugin, package: "venice-ai-sdk-provider", provider: "custom-provider.chat" },
|
||||
] as const
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ import { Effect } from "effect"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
|
||||
describe("Provider", () => {
|
||||
test("loads Vertex native provider entrypoints", async () => {
|
||||
test("loads bundled native provider entrypoints", async () => {
|
||||
const packages = [
|
||||
"@opencode-ai/ai/providers/cerebras",
|
||||
"@opencode-ai/ai/providers/google-vertex",
|
||||
"@opencode-ai/ai/providers/google-vertex/gemini",
|
||||
"@opencode-ai/ai/providers/google-vertex/chat",
|
||||
"@opencode-ai/ai/providers/google-vertex/responses",
|
||||
"@opencode-ai/ai/providers/google-vertex/messages",
|
||||
"@opencode-ai/ai/providers/togetherai",
|
||||
]
|
||||
|
||||
for (const specifier of packages) {
|
||||
|
||||
Reference in New Issue
Block a user