From 6c97be69748dfd4337bb80e3ca3a933051d37844 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:20:47 -0500 Subject: [PATCH] feat(ai): add native Cerebras and Together AI providers (#45098) --- bun.lock | 8 - packages/ai/script/setup-recording-env.ts | 10 +- packages/ai/src/providers/cerebras.ts | 58 +++++++ packages/ai/src/providers/index.ts | 2 + packages/ai/src/providers/togetherai.ts | 58 +++++++ .../cerebras-gpt-oss-120b-text.json | 32 ++++ .../cerebras-gpt-oss-120b-tool-call.json | 32 ++++ .../cerebras-gpt-oss-120b-tool-loop.json | 50 ++++++ packages/ai/test/provider-package.test.ts | 2 + .../ai/test/provider/golden.recorded.test.ts | 33 +++- .../ai/test/provider/native-providers.test.ts | 151 ++++++++++++++++++ packages/ai/test/recorded-golden.ts | 2 + packages/ai/test/recorded-scenarios.ts | 3 +- packages/core/package.json | 2 - packages/core/src/aisdk-native.ts | 11 ++ packages/core/src/model-resolver.ts | 4 + packages/core/src/plugin/provider.ts | 2 - packages/core/src/plugin/provider/cerebras.ts | 12 +- .../core/src/plugin/provider/togetherai.ts | 10 -- packages/core/src/provider.ts | 2 + packages/core/test/aisdk-native.test.ts | 27 ++++ packages/core/test/model-resolver.test.ts | 26 +++ .../test/plugin/provider-cerebras.test.ts | 79 ++------- .../core/test/plugin/provider-factory.test.ts | 2 - packages/core/test/provider.test.ts | 4 +- 25 files changed, 507 insertions(+), 115 deletions(-) create mode 100644 packages/ai/src/providers/cerebras.ts create mode 100644 packages/ai/src/providers/togetherai.ts create mode 100644 packages/ai/test/fixtures/recordings/cerebras-chat/cerebras-gpt-oss-120b-text.json create mode 100644 packages/ai/test/fixtures/recordings/cerebras-chat/cerebras-gpt-oss-120b-tool-call.json create mode 100644 packages/ai/test/fixtures/recordings/cerebras-chat/cerebras-gpt-oss-120b-tool-loop.json create mode 100644 packages/ai/test/provider/native-providers.test.ts delete mode 100644 packages/core/src/plugin/provider/togetherai.ts diff --git a/bun.lock b/bun.lock index 5c2737b38bb..5baa5c009f2 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/packages/ai/script/setup-recording-env.ts b/packages/ai/script/setup-recording-env.ts index 3ee8189141e..32056144e43 100644 --- a/packages/ai/script/setup-recording-env.ts +++ b/packages/ai/script/setup-recording-env.ts @@ -157,9 +157,9 @@ const PROVIDERS: ReadonlyArray = [ 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 = [ { 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)), }, diff --git a/packages/ai/src/providers/cerebras.ts b/packages/ai/src/providers/cerebras.ts new file mode 100644 index 00000000000..1c53cdef85c --- /dev/null +++ b/packages/ai/src/providers/cerebras.ts @@ -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 & + 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({ + id: modelID, + compatibility: { maxTokensField: "max_tokens", reasoningField: "reasoning", supportsStore: false }, + }), + configure, + } +} + +export const provider = configure() + +export const model: ProviderPackage.Definition["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) diff --git a/packages/ai/src/providers/index.ts b/packages/ai/src/providers/index.ts index 46f7ec7f0f2..4bc1cc2712f 100644 --- a/packages/ai/src/providers/index.ts +++ b/packages/ai/src/providers/index.ts @@ -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" diff --git a/packages/ai/src/providers/togetherai.ts b/packages/ai/src/providers/togetherai.ts new file mode 100644 index 00000000000..fed147e629a --- /dev/null +++ b/packages/ai/src/providers/togetherai.ts @@ -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 & + 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({ + id: modelID, + compatibility: { maxTokensField: "max_tokens", supportsStore: false, supportsStrictMode: false }, + }), + configure, + } +} + +export const provider = configure() + +export const model: ProviderPackage.Definition["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) diff --git a/packages/ai/test/fixtures/recordings/cerebras-chat/cerebras-gpt-oss-120b-text.json b/packages/ai/test/fixtures/recordings/cerebras-chat/cerebras-gpt-oss-120b-text.json new file mode 100644 index 00000000000..73b91d26428 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/cerebras-chat/cerebras-gpt-oss-120b-text.json @@ -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" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/cerebras-chat/cerebras-gpt-oss-120b-tool-call.json b/packages/ai/test/fixtures/recordings/cerebras-chat/cerebras-gpt-oss-120b-tool-call.json new file mode 100644 index 00000000000..3f370bbd6ee --- /dev/null +++ b/packages/ai/test/fixtures/recordings/cerebras-chat/cerebras-gpt-oss-120b-tool-call.json @@ -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" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/cerebras-chat/cerebras-gpt-oss-120b-tool-loop.json b/packages/ai/test/fixtures/recordings/cerebras-chat/cerebras-gpt-oss-120b-tool-loop.json new file mode 100644 index 00000000000..f2740f22ca5 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/cerebras-chat/cerebras-gpt-oss-120b-tool-loop.json @@ -0,0 +1,50 @@ +{ + "version": 1, + "metadata": { + "provider": "cerebras", + "route": "cerebras-chat", + "transport": "http", + "model": "gpt-oss-120b", + "tags": ["prefix:cerebras-chat", "provider:cerebras", "tool", "tool-loop", "golden"], + "name": "cerebras-chat/cerebras-gpt-oss-120b-tool-loop", + "recordedAt": "2026-08-25T23:55:28.827Z" + }, + "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\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":\"What is the weather in 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}}],\"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-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"role\":\"assistant\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\"The\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\" user asks: \\\"What is the weather in Paris?\\\" The developer says: Use get\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\"_weather tool exactly once. After the tool result, reply exactly: \\\"Paris\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\" is sunny.\\\"\\n\\nSo we need to call the function get_weather\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\" with city: \\\"Paris\\\". Then after getting\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\" result, we need to output exactly \\\"Paris is sunny\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\".\\\"\\n\\nWe must obey that after tool\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\" result (i.e., the tool\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\"'s output) we reply\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\" exactly that string. So\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\" we should call the function\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\", then produce that answer\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\".\\n\\nWe must follow\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\" guidelines: we should not add\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\" any extra text. So we will\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"reasoning\":\" call the function.\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"},\"type\":\"function\",\"id\":\"c882d2434\",\"index\":0}]},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"function\":{\"arguments\":\"{\\\"city\"},\"type\":\"function\",\"index\":0}]},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"function\":{\"arguments\":\"\\\":\\\"Paris\\\"}\"},\"type\":\"function\",\"index\":0}]},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-cffbdca2-64ee-487b-babe-0a02970550f9\",\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\",\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\",\"usage\":{\"total_tokens\":294,\"completion_tokens\":144,\"completion_tokens_details\":{\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0,\"reasoning_tokens\":120},\"prompt_tokens\":150,\"prompt_tokens_details\":{\"cached_tokens\":0}},\"time_info\":{\"created\":1787702128.6481314,\"queue_time\":0.003597903,\"prompt_time\":0.00255912,\"completion_time\":0.087462961,\"total_time\":0.09496760368347168}}\n\ndata: [DONE]\n\n" + } + }, + { + "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\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"c882d2434\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}],\"reasoning\":\"The user asks: \\\"What is the weather in Paris?\\\" The developer says: Use get_weather tool exactly once. After the tool result, reply exactly: \\\"Paris is sunny.\\\"\\n\\nSo we need to call the function get_weather with city: \\\"Paris\\\". Then after getting result, we need to output exactly \\\"Paris is sunny.\\\"\\n\\nWe must obey that after tool result (i.e., the tool's output) we reply exactly that string. So we should call the function, then produce that answer.\\n\\nWe must follow guidelines: we should not add any extra text. So we will call the function.\"},{\"role\":\"tool\",\"tool_call_id\":\"c882d2434\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"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}}],\"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-26966d77-cb4c-4d07-b435-c2ad4279f271\",\"choices\":[{\"delta\":{\"role\":\"assistant\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-26966d77-cb4c-4d07-b435-c2ad4279f271\",\"choices\":[{\"delta\":{\"content\":\"Paris\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-26966d77-cb4c-4d07-b435-c2ad4279f271\",\"choices\":[{\"delta\":{\"content\":\" is sunny.\"},\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-26966d77-cb4c-4d07-b435-c2ad4279f271\",\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\",\"index\":0}],\"created\":1787702128,\"model\":\"gpt-oss-120b\",\"system_fingerprint\":\"fp_4cfabdd6620dc0120785\",\"object\":\"chat.completion.chunk\",\"usage\":{\"total_tokens\":200,\"completion_tokens\":8,\"completion_tokens_details\":{\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0,\"reasoning_tokens\":0},\"prompt_tokens\":192,\"prompt_tokens_details\":{\"cached_tokens\":128}},\"time_info\":{\"created\":1787702128.8720248,\"queue_time\":0.003610424,\"prompt_time\":0.005012001,\"completion_time\":0.00146802,\"total_time\":0.012445449829101562}}\n\ndata: [DONE]\n\n" + } + } + ] +} diff --git a/packages/ai/test/provider-package.test.ts b/packages/ai/test/provider-package.test.ts index 2af6f32754b..fafdac48f4f 100644 --- a/packages/ai/test/provider-package.test.ts +++ b/packages/ai/test/provider-package.test.ts @@ -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() diff --git a/packages/ai/test/provider/golden.recorded.test.ts b/packages/ai/test/provider/golden.recorded.test.ts index f6f8dfb9c60..581c354325e 100644 --- a/packages/ai/test/provider/golden.recorded.test.ts +++ b/packages/ai/test/provider/golden.recorded.test.ts @@ -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", diff --git a/packages/ai/test/provider/native-providers.test.ts b/packages/ai/test/provider/native-providers.test.ts new file mode 100644 index 00000000000..4fdbd3a0081 --- /dev/null +++ b/packages/ai/test/provider/native-providers.test.ts @@ -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"))), + ), + ) + }), + ) +}) diff --git a/packages/ai/test/recorded-golden.ts b/packages/ai/test/recorded-golden.ts index 5657e90d226..eff68ade9f9 100644 --- a/packages/ai/test/recorded-golden.ts +++ b/packages/ai/test/recorded-golden.ts @@ -20,6 +20,7 @@ type ScenarioInput = readonly name?: string readonly cassette?: string readonly tags?: ReadonlyArray + 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, }) diff --git a/packages/ai/test/recorded-scenarios.ts b/packages/ai/test/recorded-scenarios.ts index 23a3c3e592e..95773c07e1f 100644 --- a/packages/ai/test/recorded-scenarios.ts +++ b/packages/ai/test/recorded-scenarios.ts @@ -164,6 +164,7 @@ export const expectGoldenWeatherToolLoop = (events: ReadonlyArray) => 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, diff --git a/packages/core/package.json b/packages/core/package.json index a46197e7ca3..4a0aa318aed 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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:", diff --git a/packages/core/src/aisdk-native.ts b/packages/core/src/aisdk-native.ts index d23424a0d40..9fdabe9cee3 100644 --- a/packages/core/src/aisdk-native.ts +++ b/packages/core/src/aisdk-native.ts @@ -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", diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts index 51bcfdab01a..56eeb644de8 100644 --- a/packages/core/src/model-resolver.ts +++ b/packages/core/src/model-resolver.ts @@ -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" || diff --git a/packages/core/src/plugin/provider.ts b/packages/core/src/plugin/provider.ts index 18a3cddfdec..ac952b1684c 100644 --- a/packages/core/src/plugin/provider.ts +++ b/packages/core/src/plugin/provider.ts @@ -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, diff --git a/packages/core/src/plugin/provider/cerebras.ts b/packages/core/src/plugin/provider/cerebras.ts index 92a4d4df2f5..43c61fd2c8e 100644 --- a/packages/core/src/plugin/provider/cerebras.ts +++ b/packages/core/src/plugin/provider/cerebras.ts @@ -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) - }), - ) }), }) diff --git a/packages/core/src/plugin/provider/togetherai.ts b/packages/core/src/plugin/provider/togetherai.ts deleted file mode 100644 index 38174d4856d..00000000000 --- a/packages/core/src/plugin/provider/togetherai.ts +++ /dev/null @@ -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) - }, -}) diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index 307c644b700..11a42bdc661 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -48,6 +48,7 @@ const builtins = new Map Promise>([ ["@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 Promise>([ ["@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")], ]) diff --git a/packages/core/test/aisdk-native.test.ts b/packages/core/test/aisdk-native.test.ts index ea464479a11..34a37668156 100644 --- a/packages/core/test/aisdk-native.test.ts +++ b/packages/core/test/aisdk-native.test.ts @@ -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", { diff --git a/packages/core/test/model-resolver.test.ts b/packages/core/test/model-resolver.test.ts index 208277560de..60cff54d82e 100644 --- a/packages/core/test/model-resolver.test.ts +++ b/packages/core/test/model-resolver.test.ts @@ -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", diff --git a/packages/core/test/plugin/provider-cerebras.test.ts b/packages/core/test/plugin/provider-cerebras.test.ts index 1a479b7d8fa..51511f6a3a3 100644 --- a/packages/core/test/plugin/provider-cerebras.test.ts +++ b/packages/core/test/plugin/provider-cerebras.test.ts @@ -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[] = [] 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) => { - 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() }), ) }) diff --git a/packages/core/test/plugin/provider-factory.test.ts b/packages/core/test/plugin/provider-factory.test.ts index 28856ad6d0b..0ffc422011b 100644 --- a/packages/core/test/plugin/provider-factory.test.ts +++ b/packages/core/test/plugin/provider-factory.test.ts @@ -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 diff --git a/packages/core/test/provider.test.ts b/packages/core/test/provider.test.ts index af254cba274..16008d09b07 100644 --- a/packages/core/test/provider.test.ts +++ b/packages/core/test/provider.test.ts @@ -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) {