feat(core): add Snowflake Cortex provider (#29901)
Co-authored-by: Cortex Code <noreply@snowflake.com>
This commit is contained in:
parent
39babcbb53
commit
2859ce6e73
6 changed files with 434 additions and 0 deletions
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue