From 691d7037384290331496f9ef0926c5ea8d9ce3cb Mon Sep 17 00:00:00 2001 From: Kamesh Sampath Date: Fri, 29 May 2026 20:17:48 +0530 Subject: [PATCH 1/4] feat(core): add Snowflake Cortex provider Snowflake Cortex uses a per-account endpoint URL and has two API quirks that require explicit handling: it expects max_completion_tokens instead of max_tokens, and returns 400 "conversation complete" as a normal stop condition rather than a finish_reason. Changes: - custom() loader in provider.ts: resolves baseURL from SNOWFLAKE_ACCOUNT, PAT from SNOWFLAKE_CORTEX_PAT or opencode auth store, fetch interceptor rewrites max_tokens and handles conversation complete - SnowflakeCortexPlugin in packages/core: V2 plugin for the aisdk.sdk hook with the same fetch interceptor; registered before OpenAICompatiblePlugin - providers.ts: dedicated login flow that collects and stores both account identifier and PAT via opencode auth Companion models.dev PR: anomalyco/models.dev#1902 .... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code) Co-Authored-By: Cortex Code --- packages/core/src/plugin/provider/index.ts | 2 + .../src/plugin/provider/snowflake-cortex.ts | 60 ++++++ .../plugin/provider-snowflake-cortex.test.ts | 172 ++++++++++++++++++ packages/opencode/src/cli/cmd/providers.ts | 19 ++ packages/opencode/src/provider/provider.ts | 89 +++++++++ 5 files changed, 342 insertions(+) create mode 100644 packages/core/src/plugin/provider/snowflake-cortex.ts create mode 100644 packages/core/test/plugin/provider-snowflake-cortex.test.ts diff --git a/packages/core/src/plugin/provider/index.ts b/packages/core/src/plugin/provider/index.ts index fd02d322a1f9..0ebbd50f617e 100644 --- a/packages/core/src/plugin/provider/index.ts +++ b/packages/core/src/plugin/provider/index.ts @@ -19,6 +19,7 @@ import { LLMGatewayPlugin } from "./llmgateway" import { MistralPlugin } from "./mistral" import { NvidiaPlugin } from "./nvidia" import { OpenAIPlugin } from "./openai" +import { SnowflakeCortexPlugin } from "./snowflake-cortex" import { OpenAICompatiblePlugin } from "./openai-compatible" import { OpencodePlugin } from "./opencode" import { OpenRouterPlugin } from "./openrouter" @@ -53,6 +54,7 @@ export const ProviderPlugins = [ MistralPlugin, NvidiaPlugin, OpencodePlugin, + SnowflakeCortexPlugin, OpenAICompatiblePlugin, OpenAIPlugin, OpenRouterPlugin, diff --git a/packages/core/src/plugin/provider/snowflake-cortex.ts b/packages/core/src/plugin/provider/snowflake-cortex.ts new file mode 100644 index 000000000000..2f6a2a5e57cd --- /dev/null +++ b/packages/core/src/plugin/provider/snowflake-cortex.ts @@ -0,0 +1,60 @@ +import { Effect } from "effect" +import { PluginV2 } from "../../plugin" +import { ProviderV2 } from "../../provider" + +type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise + +// Exported for testing: intercepts Cortex-specific request/response quirks. +export function cortexFetch(upstream: FetchLike = fetch) { + return async (url: string | URL | Request, init?: RequestInit): Promise => { + if (init?.body && typeof init.body === "string") { + try { + const body = JSON.parse(init.body) + if ("max_tokens" in body) { + body.max_completion_tokens = body.max_tokens + delete body.max_tokens + init = { ...init, body: JSON.stringify(body) } + } + } catch {} + } + + const response = await upstream(url, init) + + // Cortex returns 400 "conversation complete" as a normal stop condition + if (!response.ok && response.status === 400) { + try { + const errorData = (await response.clone().json()) as Record + if (String(errorData.message || errorData.error || "").toLowerCase().includes("conversation complete")) { + return new Response( + JSON.stringify({ choices: [{ finish_reason: "stop", message: { content: "", role: "assistant" } }] }), + { status: 200, headers: new Headers({ "content-type": "application/json" }) }, + ) + } + } catch {} + } + + return response + } +} + +export const SnowflakeCortexPlugin = PluginV2.define({ + id: PluginV2.ID.make("snowflake-cortex"), + effect: Effect.gen(function* () { + return { + "aisdk.sdk": Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return + const pat = + process.env.SNOWFLAKE_CORTEX_PAT ?? + (typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined) + const upstream = typeof evt.options.fetch === "function" ? (evt.options.fetch as FetchLike) : undefined + if (evt.options.includeUsage !== false) evt.options.includeUsage = true + const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible")) + evt.sdk = mod.createOpenAICompatible({ + ...evt.options, + ...(pat ? { apiKey: pat } : {}), + fetch: cortexFetch(upstream) as typeof fetch, + } as any) + }), + } + }), +}) diff --git a/packages/core/test/plugin/provider-snowflake-cortex.test.ts b/packages/core/test/plugin/provider-snowflake-cortex.test.ts new file mode 100644 index 000000000000..4c28037ef70b --- /dev/null +++ b/packages/core/test/plugin/provider-snowflake-cortex.test.ts @@ -0,0 +1,172 @@ +import { describe, expect, it as bun_it } from "bun:test" +import { Effect } from "effect" +import { PluginV2 } from "@opencode-ai/core/plugin" +import { SnowflakeCortexPlugin, cortexFetch } from "@opencode-ai/core/plugin/provider/snowflake-cortex" +import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" +import { expectPluginRegistered, it, model, withEnv } from "./provider-helper" + +describe("SnowflakeCortexPlugin", () => { + it.effect("is registered in ProviderPlugins before OpenAICompatiblePlugin", () => + Effect.sync(() => { + expectPluginRegistered( + ProviderPlugins.map((item) => item.id), + "snowflake-cortex", + ) + const ids = ProviderPlugins.map((p) => p.id as string) + expect(ids.indexOf("snowflake-cortex")).toBeLessThan(ids.indexOf("openai-compatible")) + }), + ) + + it.effect("ignores non-snowflake-cortex providers", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(SnowflakeCortexPlugin) + const result = yield* plugin.trigger( + "aisdk.sdk", + { model: model("openai", "gpt-4"), package: "@ai-sdk/openai", options: { name: "openai" } }, + {}, + ) + expect(result.sdk).toBeUndefined() + }), + ) + + it.effect("creates SDK for snowflake-cortex using SNOWFLAKE_CORTEX_PAT env var", () => + withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(SnowflakeCortexPlugin) + const result = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("snowflake-cortex", "claude-sonnet-4-6"), + package: "@ai-sdk/openai-compatible", + options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" }, + }, + {}, + ) + expect(result.sdk).toBeDefined() + }), + ), + ) + + it.effect("falls back to options.apiKey when SNOWFLAKE_CORTEX_PAT env var is absent", () => + withEnv({ SNOWFLAKE_CORTEX_PAT: undefined }, () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(SnowflakeCortexPlugin) + const result = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("snowflake-cortex", "claude-sonnet-4-6"), + package: "@ai-sdk/openai-compatible", + options: { + name: "snowflake-cortex", + baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1", + apiKey: "options-pat", + }, + }, + {}, + ) + expect(result.sdk).toBeDefined() + }), + ), + ) + + it.effect("sets includeUsage on the SDK options", () => + withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + const captured: Record[] = [] + yield* plugin.add(SnowflakeCortexPlugin) + yield* plugin.add({ + id: PluginV2.ID.make("inspector"), + effect: Effect.succeed({ + "aisdk.sdk": (evt) => + Effect.sync(() => { + captured.push({ ...evt.options }) + }), + }), + }) + yield* plugin.trigger( + "aisdk.sdk", + { + model: model("snowflake-cortex", "claude-sonnet-4-6"), + package: "@ai-sdk/openai-compatible", + options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" }, + }, + {}, + ) + expect(captured[0]?.includeUsage).toBe(true) + }), + ), + ) +}) + +type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise + +describe("cortexFetch", () => { + bun_it("rewrites max_tokens to max_completion_tokens", async () => { + const captured: RequestInit[] = [] + const upstream: FetchLike = async (_url, init) => { + captured.push(init ?? {}) + return new Response("{}", { status: 200 }) + } + await cortexFetch(upstream)("https://test", { + method: "POST", + body: JSON.stringify({ model: "claude-sonnet-4-6", max_tokens: 1024 }), + }) + const body = JSON.parse(captured[0].body as string) + expect(body.max_completion_tokens).toBe(1024) + expect(body.max_tokens).toBeUndefined() + }) + + bun_it("preserves body when max_tokens is absent", async () => { + const captured: RequestInit[] = [] + const upstream: FetchLike = async (_url, init) => { + captured.push(init ?? {}) + return new Response("{}", { status: 200 }) + } + const original = JSON.stringify({ model: "claude-sonnet-4-6", temperature: 0.7 }) + await cortexFetch(upstream)("https://test", { method: "POST", body: original }) + expect(captured[0].body).toBe(original) + }) + + bun_it("treats 400 'conversation complete' as a stop response", async () => { + const upstream: FetchLike = async () => + new Response(JSON.stringify({ message: "Conversation complete" }), { + status: 400, + headers: { "content-type": "application/json" }, + }) + const response = await cortexFetch(upstream)("https://test", {}) + expect(response.status).toBe(200) + const data = (await response.json()) as { choices: { finish_reason: string }[] } + expect(data.choices[0].finish_reason).toBe("stop") + }) + + bun_it("passes through other 400 errors unchanged", async () => { + const upstream: FetchLike = async () => + new Response(JSON.stringify({ message: "Invalid model" }), { + status: 400, + headers: { "content-type": "application/json" }, + }) + const response = await cortexFetch(upstream)("https://test", {}) + expect(response.status).toBe(400) + }) + + bun_it("passes through non-400 errors unchanged", async () => { + const upstream: FetchLike = async () => new Response("Unauthorized", { status: 401 }) + const response = await cortexFetch(upstream)("https://test", {}) + expect(response.status).toBe(401) + }) + + bun_it("handles invalid JSON body gracefully without throwing", async () => { + const captured: RequestInit[] = [] + const upstream: FetchLike = async (_url, init) => { + captured.push(init ?? {}) + return new Response("{}", { status: 200 }) + } + const invalidBody = "{ not json }" + await cortexFetch(upstream)("https://test", { method: "POST", body: invalidBody }) + expect(captured[0].body).toBe(invalidBody) + }) +}) diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 560781ef0e43..639d868cd8dd 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -474,6 +474,25 @@ export const ProvidersLoginCommand = effectCmd({ ) } + if (provider === "snowflake-cortex") { + const account = yield* promptValue( + yield* Prompt.text({ + message: "Snowflake Account Identifier", + placeholder: "xy12345.us-east-1", + validate: (x) => (x && x.length > 0 ? undefined : "Required"), + }), + ) + const pat = yield* promptValue( + yield* Prompt.password({ + message: "Programmatic Access Token (PAT)", + validate: (x) => (x && x.length > 0 ? undefined : "Required"), + }), + ) + yield* Effect.orDie(authSvc.set(provider, { type: "api", key: pat, metadata: { account } })) + yield* Prompt.outro("Done") + return + } + const key = yield* Prompt.password({ message: "Enter your API key", validate: (x) => (x && x.length > 0 ? undefined : "Required"), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 6f3cfccc5143..8dbfc3d04a04 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -858,6 +858,95 @@ function custom(dep: CustomDep): Record { }, }, }), + "snowflake-cortex": Effect.fnUntraced(function* (input: Info) { + const env = yield* dep.env() + const auth = yield* dep.auth(input.id) + + const account = + env["SNOWFLAKE_ACCOUNT"] ?? + (auth?.type === "api" ? auth.metadata?.account : undefined) ?? + input.options?.account + + const pat = + env["SNOWFLAKE_CORTEX_PAT"] ?? + (auth?.type === "api" ? auth.key : undefined) ?? + input.options?.apiKey + + if (!account || !pat) { + const missing = [!account && "SNOWFLAKE_ACCOUNT", !pat && "SNOWFLAKE_CORTEX_PAT"].filter(Boolean).join(", ") + return { + autoload: false, + async getModel() { + throw new Error( + `Snowflake Cortex: missing credentials (${missing}). Set via env var, opencode auth, or provider options.`, + ) + }, + } + } + + const baseURL = `https://${account}.snowflakecomputing.com/api/v2/cortex/v1` + + return { + autoload: input.source === "config", + options: { + baseURL, + apiKey: pat, + fetch: async (url: RequestInfo | URL, init?: RequestInit) => { + if (init?.body && typeof init.body === "string") { + try { + const body = JSON.parse(init.body) + if ("max_tokens" in body) { + body.max_completion_tokens = body.max_tokens + delete body.max_tokens + init = { ...init, body: JSON.stringify(body) } + } + } catch {} + } + + const response = await fetch(url, init) + + // Cortex returns 400 "conversation complete" as a normal stop condition + if (!response.ok && response.status === 400) { + try { + const errorData = await response.clone().json() + const errorMessage = String(errorData.message || errorData.error || "") + if (errorMessage.toLowerCase().includes("conversation complete")) { + return new Response( + JSON.stringify({ choices: [{ finish_reason: "stop", message: { content: "", role: "assistant" } }] }), + { status: 200, headers: new Headers({ "content-type": "application/json" }) }, + ) + } + } catch {} + } + + // Normalize streaming chunks: Snowflake returns role:"" instead of role:"assistant" + if (response.body && response.headers.get("content-type")?.includes("text/event-stream")) { + const reader = response.body.getReader() + const encoder = new TextEncoder() + const decoder = new TextDecoder() + const stream = new ReadableStream({ + async pull(ctrl) { + const { done, value } = await reader.read() + if (done) { + ctrl.close() + return + } + const text = decoder.decode(value) + const fixed = text.replace(/"role"\s*:\s*""/g, '"role":"assistant"') + ctrl.enqueue(encoder.encode(fixed)) + }, + cancel() { + reader.cancel() + }, + }) + return new Response(stream, { headers: response.headers, status: response.status }) + } + + return response + }, + }, + } + }), } } From 2c43c100650e06bf30bd6943f8f0689ded151c52 Mon Sep 17 00:00:00 2001 From: Kamesh Sampath Date: Sat, 30 May 2026 09:23:03 +0530 Subject: [PATCH 2/4] fix(core): fix TextDecoder streaming bug and add SSE role normalizer to V2 plugin Cortex returns role:"" in streaming delta chunks for GPT-5 models; the AI SDK schema validation rejects this because the only valid value is "assistant". The legacy provider.ts already had the SSE normalizer but used decoder.decode() without { stream: true }, which corrupts multi-byte chars at chunk boundaries. - Fix decoder.decode to use { stream: true } in provider.ts - Add the same SSE normalizer to cortexFetch in the V2 plugin - Add test for the role normalization .... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code) Co-Authored-By: Cortex Code --- .../src/plugin/provider/snowflake-cortex.ts | 21 +++++++++++++++++++ .../plugin/provider-snowflake-cortex.test.ts | 13 ++++++++++++ packages/opencode/src/provider/provider.ts | 7 +++---- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/core/src/plugin/provider/snowflake-cortex.ts b/packages/core/src/plugin/provider/snowflake-cortex.ts index 2f6a2a5e57cd..3493b976bda8 100644 --- a/packages/core/src/plugin/provider/snowflake-cortex.ts +++ b/packages/core/src/plugin/provider/snowflake-cortex.ts @@ -33,6 +33,27 @@ export function cortexFetch(upstream: FetchLike = fetch) { } catch {} } + // Cortex returns role:"" in streaming deltas; the AI SDK schema requires "assistant" + if (response.body && response.headers.get("content-type")?.includes("text/event-stream")) { + const reader = response.body.getReader() + const encoder = new TextEncoder() + const decoder = new TextDecoder() + const stream = new ReadableStream({ + async pull(ctrl) { + const { done, value } = await reader.read() + if (done) { + ctrl.close() + return + } + ctrl.enqueue(encoder.encode(decoder.decode(value, { stream: true }).replace(/"role"\s*:\s*""/g, '"role":"assistant"'))) + }, + cancel() { + reader.cancel() + }, + }) + return new Response(stream, { headers: response.headers, status: response.status }) + } + return response } } diff --git a/packages/core/test/plugin/provider-snowflake-cortex.test.ts b/packages/core/test/plugin/provider-snowflake-cortex.test.ts index 4c28037ef70b..cec25dbf0562 100644 --- a/packages/core/test/plugin/provider-snowflake-cortex.test.ts +++ b/packages/core/test/plugin/provider-snowflake-cortex.test.ts @@ -169,4 +169,17 @@ describe("cortexFetch", () => { await cortexFetch(upstream)("https://test", { method: "POST", body: invalidBody }) expect(captured[0].body).toBe(invalidBody) }) + + bun_it("rewrites role:'' to role:'assistant' in streaming SSE chunks", async () => { + const chunk = `data: {"choices":[{"delta":{"role":"","content":"Hi"},"index":0}]}\n\n` + const upstream: FetchLike = async () => + new Response(new ReadableStream({ start: (ctrl) => { ctrl.enqueue(new TextEncoder().encode(chunk)); ctrl.close() } }), { + status: 200, + headers: { "content-type": "text/event-stream" }, + }) + const response = await cortexFetch(upstream)("https://test", {}) + const text = await response.text() + expect(text).toContain('"role":"assistant"') + expect(text).not.toContain('"role":""') + }) }) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 8dbfc3d04a04..21b11d80e959 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -919,7 +919,7 @@ function custom(dep: CustomDep): Record { } catch {} } - // Normalize streaming chunks: Snowflake returns role:"" instead of role:"assistant" + // Cortex returns role:"" in streaming deltas; the AI SDK schema requires "assistant" if (response.body && response.headers.get("content-type")?.includes("text/event-stream")) { const reader = response.body.getReader() const encoder = new TextEncoder() @@ -931,9 +931,8 @@ function custom(dep: CustomDep): Record { ctrl.close() return } - const text = decoder.decode(value) - const fixed = text.replace(/"role"\s*:\s*""/g, '"role":"assistant"') - ctrl.enqueue(encoder.encode(fixed)) + const text = decoder.decode(value, { stream: true }) + ctrl.enqueue(encoder.encode(text.replace(/"role"\s*:\s*""/g, '"role":"assistant"'))) }, cancel() { reader.cancel() From 8a9bff0f41fef5ef4225274f085f7aee8134e036 Mon Sep 17 00:00:00 2001 From: Kamesh Sampath Date: Sat, 30 May 2026 09:47:20 +0530 Subject: [PATCH 3/4] docs: add Snowflake Cortex provider documentation Adds setup guide for Snowflake Cortex to the providers page, covering the /connect flow, env var alternative, and a sample opencode.json with model capability flags. .... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code) Co-Authored-By: Cortex Code --- packages/web/src/content/docs/providers.mdx | 70 +++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index f5c2160daaf4..657e7be740c0 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -1940,6 +1940,76 @@ To use [Scaleway Generative APIs](https://www.scaleway.com/en/docs/generative-ap --- +### Snowflake Cortex + +[Snowflake Cortex](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-rest-api) gives you access to frontier models (Claude, OpenAI GPT-5, Llama, Mistral, and more) via an OpenAI-compatible API. All inference runs within the Snowflake perimeter. Billed in Snowflake credits — see the [Snowflake Service Consumption Table](https://www.snowflake.com/legal-files/CreditConsumptionTable.pdf) for per-model rates. + +1. Generate a [Programmatic Access Token (PAT)](https://docs.snowflake.com/en/user-guide/programmatic-access-tokens) in your Snowflake account. + +2. Run the `/connect` command and search for **Snowflake Cortex**. + + ```txt + /connect + ``` + +3. Enter your **account identifier** when prompted (e.g. `xy12345.us-east-1`). + + ```txt + ┌ Snowflake Account Identifier + │ + │ + └ enter + ``` + +4. Enter your **PAT**. + + ```txt + ┌ Programmatic Access Token (PAT) + │ + │ + └ enter + ``` + +5. Run the `/models` command to select a model. + + ```txt + /models + ``` + +Alternatively, set environment variables before starting opencode: + +```bash +export SNOWFLAKE_ACCOUNT=xy12345.us-east-1 +export SNOWFLAKE_CORTEX_PAT=your-pat +``` + +You can also configure the provider and models directly in `opencode.json`: + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "model": "snowflake-cortex/claude-sonnet-4-6", + "small_model": "snowflake-cortex/claude-haiku-4-5", + "provider": { + "snowflake-cortex": { + "name": "Snowflake Cortex", + "models": { + "claude-sonnet-4-6": { "name": "Claude Sonnet 4.6", "tool_call": true, "attachment": true }, + "claude-haiku-4-5": { "name": "Claude Haiku 4.5", "tool_call": true, "attachment": true }, + "claude-opus-4-7": { "name": "Claude Opus 4.7", "tool_call": true, "attachment": true, "reasoning": true }, + "openai-gpt-5": { "name": "GPT-5", "tool_call": true, "attachment": true }, + "llama4-maverick": { "name": "Llama 4 Maverick", "tool_call": false }, + "deepseek-r1": { "name": "DeepSeek R1", "tool_call": false, "reasoning": true } + } + } + } +} +``` + +Note: models with `"tool_call": false` (Llama, Mistral, DeepSeek) work for chat but cannot use opencode's file editing and shell tools. + +--- + ### Together AI 1. Head over to the [Together AI console](https://api.together.ai), create an account, and click **Add Key**. From e33904ab7a95d89d16f1d809d782115672a938af Mon Sep 17 00:00:00 2001 From: Kamesh Sampath Date: Sat, 30 May 2026 10:50:15 +0530 Subject: [PATCH 4/4] docs: update Snowflake Cortex provider docs - Add trial signup link with UTM tracking - Link to official account identifier and trial account docs - Clarify that the provider is limited to tool-calling capable models - Simplify opencode.json example to rely on models.dev catalog - Remove em dashes and stale model list --- packages/web/src/content/docs/providers.mdx | 29 +++++++-------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 657e7be740c0..628e2244e03c 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -1942,7 +1942,11 @@ To use [Scaleway Generative APIs](https://www.scaleway.com/en/docs/generative-ap ### Snowflake Cortex -[Snowflake Cortex](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-rest-api) gives you access to frontier models (Claude, OpenAI GPT-5, Llama, Mistral, and more) via an OpenAI-compatible API. All inference runs within the Snowflake perimeter. Billed in Snowflake credits — see the [Snowflake Service Consumption Table](https://www.snowflake.com/legal-files/CreditConsumptionTable.pdf) for per-model rates. +[Snowflake Cortex](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-rest-api) gives you access to frontier models (Claude, OpenAI GPT-5, and more) via an OpenAI-compatible API. All inference runs within the Snowflake perimeter and is billed in Snowflake credits. For per-model rates, see the [Snowflake Service Consumption Table](https://www.snowflake.com/legal-files/CreditConsumptionTable.pdf). + +Don't have a Snowflake account? [Sign up for a free trial](https://signup.snowflake.com/?utm_source=opencode&utm_medium=docs&utm_campaign=cortex-provider). + +opencode's core workflow for coding, editing files, and running commands relies on tool calling. Only the Claude and OpenAI families within Snowflake Cortex support this. The provider is limited to those families to support the core workflow. 1. Generate a [Programmatic Access Token (PAT)](https://docs.snowflake.com/en/user-guide/programmatic-access-tokens) in your Snowflake account. @@ -1952,7 +1956,7 @@ To use [Scaleway Generative APIs](https://www.scaleway.com/en/docs/generative-ap /connect ``` -3. Enter your **account identifier** when prompted (e.g. `xy12345.us-east-1`). +3. Enter your [account identifier](https://docs.snowflake.com/en/user-guide/admin-account-identifier) when prompted (e.g. `myorg-myaccount` or `xy12345.us-east-1`). ```txt ┌ Snowflake Account Identifier @@ -1979,35 +1983,20 @@ To use [Scaleway Generative APIs](https://www.scaleway.com/en/docs/generative-ap Alternatively, set environment variables before starting opencode: ```bash -export SNOWFLAKE_ACCOUNT=xy12345.us-east-1 +export SNOWFLAKE_ACCOUNT=myorg-myaccount export SNOWFLAKE_CORTEX_PAT=your-pat ``` -You can also configure the provider and models directly in `opencode.json`: +The model catalog is provided automatically. A minimal `opencode.json` is all that's needed: ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", "model": "snowflake-cortex/claude-sonnet-4-6", - "small_model": "snowflake-cortex/claude-haiku-4-5", - "provider": { - "snowflake-cortex": { - "name": "Snowflake Cortex", - "models": { - "claude-sonnet-4-6": { "name": "Claude Sonnet 4.6", "tool_call": true, "attachment": true }, - "claude-haiku-4-5": { "name": "Claude Haiku 4.5", "tool_call": true, "attachment": true }, - "claude-opus-4-7": { "name": "Claude Opus 4.7", "tool_call": true, "attachment": true, "reasoning": true }, - "openai-gpt-5": { "name": "GPT-5", "tool_call": true, "attachment": true }, - "llama4-maverick": { "name": "Llama 4 Maverick", "tool_call": false }, - "deepseek-r1": { "name": "DeepSeek R1", "tool_call": false, "reasoning": true } - } - } - } + "small_model": "snowflake-cortex/claude-haiku-4-5" } ``` -Note: models with `"tool_call": false` (Llama, Mistral, DeepSeek) work for chat but cannot use opencode's file editing and shell tools. - --- ### Together AI