feat(core): add Snowflake Cortex provider (#29901)

Co-authored-by: Cortex Code <noreply@snowflake.com>
This commit is contained in:
Kamesh Sampath 2026-06-05 08:49:55 +05:30 committed by GitHub
commit 2859ce6e73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 434 additions and 0 deletions

View file

@ -19,6 +19,7 @@ import { LLMGatewayPlugin } from "./provider/llmgateway"
import { MistralPlugin } from "./provider/mistral"
import { NvidiaPlugin } from "./provider/nvidia"
import { OpenAIPlugin } from "./provider/openai"
import { SnowflakeCortexPlugin } from "./provider/snowflake-cortex"
import { OpenAICompatiblePlugin } from "./provider/openai-compatible"
import { OpencodePlugin } from "./provider/opencode"
import { OpenRouterPlugin } from "./provider/openrouter"
@ -53,6 +54,7 @@ export const ProviderPlugins = [
MistralPlugin,
NvidiaPlugin,
OpencodePlugin,
SnowflakeCortexPlugin,
OpenAICompatiblePlugin,
OpenAIPlugin,
OpenRouterPlugin,

View file

@ -0,0 +1,81 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
// Exported for testing: intercepts Cortex-specific request/response quirks.
export function cortexFetch(upstream: FetchLike = fetch) {
return async (url: string | URL | Request, init?: RequestInit): Promise<Response> => {
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<string, unknown>
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 {}
}
// 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
}
}
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)
}),
}
}),
})

View file

@ -0,0 +1,185 @@
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<string, unknown>[] = []
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<Response>
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)
})
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":""')
})
})

View file

@ -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"),

View file

@ -851,6 +851,94 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
},
},
}),
"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 {}
}
// 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
}
const text = decoder.decode(value, { stream: true })
ctrl.enqueue(encoder.encode(text.replace(/"role"\s*:\s*""/g, '"role":"assistant"')))
},
cancel() {
reader.cancel()
},
})
return new Response(stream, { headers: response.headers, status: response.status })
}
return response
},
},
}
}),
}
}

View file

@ -1940,6 +1940,65 @@ 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, 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.
2. Run the `/connect` command and search for **Snowflake Cortex**.
```txt
/connect
```
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
└ 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=myorg-myaccount
export SNOWFLAKE_CORTEX_PAT=your-pat
```
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"
}
```
---
### Together AI
1. Head over to the [Together AI console](https://api.together.ai), create an account, and click **Add Key**.