Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/src/plugin/provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -53,6 +54,7 @@ export const ProviderPlugins = [
MistralPlugin,
NvidiaPlugin,
OpencodePlugin,
SnowflakeCortexPlugin,
OpenAICompatiblePlugin,
OpenAIPlugin,
OpenRouterPlugin,
Expand Down
60 changes: 60 additions & 0 deletions packages/core/src/plugin/provider/snowflake-cortex.ts
Original file line number Diff line number Diff line change
@@ -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<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 {}
}

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)
}),
}
}),
})
172 changes: 172 additions & 0 deletions packages/core/test/plugin/provider-snowflake-cortex.test.ts
Original file line number Diff line number Diff line change
@@ -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<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)
})
})
19 changes: 19 additions & 0 deletions packages/opencode/src/cli/cmd/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
66 changes: 66 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,72 @@ 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 {}
}

return response
},
},
}
}),
}
}

Expand Down
Loading