diff --git a/packages/ai/README.md b/packages/ai/README.md index 631bf8441..f2c75f976 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -97,6 +97,12 @@ Unified LLM API with provider collections, automatic auth resolution, token and - **Xiaomi MiMo** (defaults to API billing endpoint, with separate Token Plan providers for `cn`/`ams`/`sgp` regions) - **Alibaba Cloud Model Studio** (DashScope pay-as-you-go, with a separate China provider) - **Z.AI API** (pay-as-you-go, with a separate China provider) +- **DeepInfra** +- **Nebius Token Factory** +- **Ollama Cloud** +- **Scaleway Generative APIs** +- **SiliconFlow** (with a separate China provider) +- **StepFun** (with a separate China provider) - **Any OpenAI-compatible API**: Ollama, vLLM, LM Studio, etc. ## Installation @@ -533,6 +539,14 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex | Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | | Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | | Xiaomi MiMo Token Plan (Singapore) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | +| DeepInfra | `DEEPINFRA_API_KEY` | +| Nebius Token Factory | `NEBIUS_API_KEY` | +| Ollama Cloud | `OLLAMA_API_KEY` | +| Scaleway Generative APIs | `SCALEWAY_API_KEY` | +| SiliconFlow | `SILICONFLOW_API_KEY` | +| SiliconFlow (China) | `SILICONFLOW_CN_API_KEY` | +| StepFun | `STEPFUN_API_KEY` | +| StepFun (China) | `STEPFUN_CN_API_KEY` | | GitHub Copilot | `COPILOT_GITHUB_TOKEN` | The Z.AI pay-as-you-go APIs (`zai-api`, `zai-api-cn`) and Alibaba Cloud Model Studio (`alibaba`, `alibaba-cn`) bill usage to the account balance, while `zai`, `zai-coding-cn`, and the Qwen Token Plan providers draw on a plan. They read separate variables, so a key set for a plan never makes the pay-as-you-go models selectable. diff --git a/packages/ai/catalog/families/deepseek.ts b/packages/ai/catalog/families/deepseek.ts index b08481db7..327e74a23 100644 --- a/packages/ai/catalog/families/deepseek.ts +++ b/packages/ai/catalog/families/deepseek.ts @@ -29,14 +29,16 @@ const AZURE_DEEPSEEK_V4_THINKING_LEVEL_MAP = { max: null, } as const; -// Alibaba Cloud Model Studio (Token Plan and pay-as-you-go) serves DeepSeek V4 -// through Qwen's own thinking format. +// Alibaba Cloud Model Studio (Token Plan and pay-as-you-go) and SiliconFlow +// serve DeepSeek V4 through the `enable_thinking` toggle (Qwen's format). const QWEN_FORMAT_PROVIDERS = new Set([ "alibaba", "alibaba-cn", "qwen-token-plan", "qwen-token-plan-cn", "qwen-token-plan-individual", + "siliconflow", + "siliconflow-cn", ]); // These hosts accept native reasoning efforts instead of DeepSeek's thinking toggle. const NATIVE_EFFORT_PROVIDERS = new Set(["openrouter", "opencode"]); diff --git a/packages/ai/catalog/openai-compatible.ts b/packages/ai/catalog/openai-compatible.ts new file mode 100644 index 000000000..69f88171d --- /dev/null +++ b/packages/ai/catalog/openai-compatible.ts @@ -0,0 +1,21 @@ +// Defaults for OpenAI-compatible hosts whose support for OpenAI-only request +// features has not been verified. Hosts that verify a feature override these. + +import type { OpenAICompletionsCompat } from "../src/types.ts"; +import type { ModelsDevModel } from "./sources/models-dev.ts"; + +/** Leave out `store`, developer messages, long cache retention, and `max_completion_tokens`. */ +export const OPENAI_COMPATIBLE_HOST_COMPAT: OpenAICompletionsCompat = { + supportsStore: false, + supportsDeveloperRole: false, + maxTokensField: "max_tokens", + supportsLongCacheRetention: false, +}; + +/** + * Send `reasoning_effort` only where models.dev lists effort levels; other + * reasoning models keep the host's default thinking behavior. + */ +export function catalogedEffortCompat(source: ModelsDevModel): OpenAICompletionsCompat { + return source.reasoningOptions?.some((option) => option.type === "effort") ? {} : { supportsReasoningEffort: false }; +} diff --git a/packages/ai/catalog/providers/deepinfra.ts b/packages/ai/catalog/providers/deepinfra.ts new file mode 100644 index 000000000..9faf786f6 --- /dev/null +++ b/packages/ai/catalog/providers/deepinfra.ts @@ -0,0 +1,14 @@ +import { defineProvider, modelsDevModels } from "../define.ts"; +import { catalogedEffortCompat, OPENAI_COMPATIBLE_HOST_COMPAT } from "../openai-compatible.ts"; + +// DeepInfra's OpenAI-compatible endpoint rejects developer messages with a 422. +export default defineProvider({ + id: "deepinfra", + models: (ctx) => + modelsDevModels(ctx, "deepinfra", { + api: "openai-completions", + baseUrl: "https://api.deepinfra.com/v1/openai", + map: (model, source) => ({ ...model, compat: catalogedEffortCompat(source) }), + }), + compat: { "openai-completions": OPENAI_COMPATIBLE_HOST_COMPAT }, +}); diff --git a/packages/ai/catalog/providers/index.ts b/packages/ai/catalog/providers/index.ts index d8502ef09..fdae4d92e 100644 --- a/packages/ai/catalog/providers/index.ts +++ b/packages/ai/catalog/providers/index.ts @@ -11,6 +11,7 @@ import baseten from "./baseten.ts"; import cerebras from "./cerebras.ts"; import cloudflareAiGateway from "./cloudflare-ai-gateway.ts"; import cloudflareWorkersAi from "./cloudflare-workers-ai.ts"; +import deepinfra from "./deepinfra.ts"; import deepseek from "./deepseek.ts"; import fireworks from "./fireworks.ts"; import githubCopilot from "./github-copilot.ts"; @@ -25,7 +26,9 @@ import minimaxCn from "./minimax-cn.ts"; import mistral from "./mistral.ts"; import moonshotai from "./moonshotai.ts"; import moonshotaiCn from "./moonshotai-cn.ts"; +import nebius from "./nebius.ts"; import nvidia from "./nvidia.ts"; +import ollamaCloud from "./ollama-cloud.ts"; import openai from "./openai.ts"; import openaiCodex from "./openai-codex.ts"; import opencode from "./opencode.ts"; @@ -35,6 +38,11 @@ import qwenTokenPlan from "./qwen-token-plan.ts"; import qwenTokenPlanCn from "./qwen-token-plan-cn.ts"; import qwenTokenPlanIndividual from "./qwen-token-plan-individual.ts"; import radius from "./radius.ts"; +import scaleway from "./scaleway.ts"; +import siliconflow from "./siliconflow.ts"; +import siliconflowCn from "./siliconflow-cn.ts"; +import stepfun from "./stepfun.ts"; +import stepfunCn from "./stepfun-cn.ts"; import together from "./together.ts"; import typesafe from "./typesafe.ts"; import vercelAiGateway from "./vercel-ai-gateway.ts"; @@ -59,6 +67,7 @@ export const PROVIDERS: readonly ProviderDefinition[] = [ cerebras, cloudflareAiGateway, cloudflareWorkersAi, + deepinfra, deepseek, fireworks, githubCopilot, @@ -73,7 +82,9 @@ export const PROVIDERS: readonly ProviderDefinition[] = [ mistral, moonshotaiCn, moonshotai, + nebius, nvidia, + ollamaCloud, openaiCodex, openai, opencodeGo, @@ -83,6 +94,11 @@ export const PROVIDERS: readonly ProviderDefinition[] = [ qwenTokenPlanIndividual, qwenTokenPlan, radius, + scaleway, + siliconflowCn, + siliconflow, + stepfunCn, + stepfun, together, typesafe, vercelAiGateway, diff --git a/packages/ai/catalog/providers/nebius.ts b/packages/ai/catalog/providers/nebius.ts new file mode 100644 index 000000000..6482fbdf8 --- /dev/null +++ b/packages/ai/catalog/providers/nebius.ts @@ -0,0 +1,14 @@ +import { defineProvider, modelsDevModels } from "../define.ts"; +import { catalogedEffortCompat, OPENAI_COMPATIBLE_HOST_COMPAT } from "../openai-compatible.ts"; + +// Nebius Token Factory. +export default defineProvider({ + id: "nebius", + models: (ctx) => + modelsDevModels(ctx, "nebius", { + api: "openai-completions", + baseUrl: "https://api.tokenfactory.nebius.com/v1", + map: (model, source) => ({ ...model, compat: catalogedEffortCompat(source) }), + }), + compat: { "openai-completions": OPENAI_COMPATIBLE_HOST_COMPAT }, +}); diff --git a/packages/ai/catalog/providers/ollama-cloud.ts b/packages/ai/catalog/providers/ollama-cloud.ts new file mode 100644 index 000000000..64dee148f --- /dev/null +++ b/packages/ai/catalog/providers/ollama-cloud.ts @@ -0,0 +1,14 @@ +import { defineProvider, modelsDevModels } from "../define.ts"; +import { catalogedEffortCompat, OPENAI_COMPATIBLE_HOST_COMPAT } from "../openai-compatible.ts"; + +// Ollama Cloud's OpenAI-compatible endpoint does not report usage while streaming. +export default defineProvider({ + id: "ollama-cloud", + models: (ctx) => + modelsDevModels(ctx, "ollama-cloud", { + api: "openai-completions", + baseUrl: "https://ollama.com/v1", + map: (model, source) => ({ ...model, compat: catalogedEffortCompat(source) }), + }), + compat: { "openai-completions": { ...OPENAI_COMPATIBLE_HOST_COMPAT, supportsUsageInStreaming: false } }, +}); diff --git a/packages/ai/catalog/providers/scaleway.ts b/packages/ai/catalog/providers/scaleway.ts new file mode 100644 index 000000000..e569c7927 --- /dev/null +++ b/packages/ai/catalog/providers/scaleway.ts @@ -0,0 +1,16 @@ +import { defineProvider, modelsDevModels } from "../define.ts"; +import { catalogedEffortCompat, OPENAI_COMPATIBLE_HOST_COMPAT } from "../openai-compatible.ts"; + +// Scaleway Generative APIs take `max_completion_tokens` and native `reasoning_effort`. +export default defineProvider({ + id: "scaleway", + models: (ctx) => + modelsDevModels(ctx, "scaleway", { + api: "openai-completions", + baseUrl: "https://api.scaleway.ai/v1", + map: (model, source) => ({ ...model, compat: catalogedEffortCompat(source) }), + }), + compat: { + "openai-completions": { ...OPENAI_COMPATIBLE_HOST_COMPAT, maxTokensField: "max_completion_tokens" }, + }, +}); diff --git a/packages/ai/catalog/providers/siliconflow-cn.ts b/packages/ai/catalog/providers/siliconflow-cn.ts new file mode 100644 index 000000000..ca1ad823e --- /dev/null +++ b/packages/ai/catalog/providers/siliconflow-cn.ts @@ -0,0 +1,9 @@ +import { defineProvider } from "../define.ts"; +import { OPENAI_COMPATIBLE_HOST_COMPAT } from "../openai-compatible.ts"; +import { siliconflowModels } from "./siliconflow.ts"; + +export default defineProvider({ + id: "siliconflow-cn", + models: (ctx) => siliconflowModels(ctx, "siliconflow-cn", "https://api.siliconflow.cn/v1"), + compat: { "openai-completions": OPENAI_COMPATIBLE_HOST_COMPAT }, +}); diff --git a/packages/ai/catalog/providers/siliconflow.ts b/packages/ai/catalog/providers/siliconflow.ts new file mode 100644 index 000000000..1c653ef2a --- /dev/null +++ b/packages/ai/catalog/providers/siliconflow.ts @@ -0,0 +1,28 @@ +import type { Model } from "../../src/types.ts"; +import { defineProvider, modelsDevModels, type ProviderContext } from "../define.ts"; +import { catalogedEffortCompat, OPENAI_COMPATIBLE_HOST_COMPAT } from "../openai-compatible.ts"; + +/** + * SiliconFlow. Reasoning models with a thinking toggle take a top-level + * `enable_thinking` flag (Qwen's thinking format); other reasoning models keep + * the server default unless models.dev lists effort levels. + */ +export function siliconflowModels(ctx: ProviderContext, source: string, baseUrl: string) { + return modelsDevModels(ctx, source, { + api: "openai-completions", + baseUrl, + map: (model, source): Model<"openai-completions"> => ({ + ...model, + compat: { + ...catalogedEffortCompat(source), + ...(source.reasoningOptions?.some((option) => option.type === "toggle") ? { thinkingFormat: "qwen" } : {}), + }, + }), + }); +} + +export default defineProvider({ + id: "siliconflow", + models: (ctx) => siliconflowModels(ctx, "siliconflow", "https://api.siliconflow.com/v1"), + compat: { "openai-completions": OPENAI_COMPATIBLE_HOST_COMPAT }, +}); diff --git a/packages/ai/catalog/providers/stepfun-cn.ts b/packages/ai/catalog/providers/stepfun-cn.ts new file mode 100644 index 000000000..ae6ddb0c1 --- /dev/null +++ b/packages/ai/catalog/providers/stepfun-cn.ts @@ -0,0 +1,8 @@ +import { defineProvider } from "../define.ts"; +import { STEPFUN_COMPLETIONS_COMPAT, stepfunModels } from "./stepfun.ts"; + +export default defineProvider({ + id: "stepfun-cn", + models: (ctx) => stepfunModels(ctx, "stepfun", "https://api.stepfun.com/v1"), + compat: { "openai-completions": STEPFUN_COMPLETIONS_COMPAT }, +}); diff --git a/packages/ai/catalog/providers/stepfun.ts b/packages/ai/catalog/providers/stepfun.ts new file mode 100644 index 000000000..d903f4f46 --- /dev/null +++ b/packages/ai/catalog/providers/stepfun.ts @@ -0,0 +1,41 @@ +import type { Model } from "../../src/types.ts"; +import { defineProvider, modelsDevModels, type ProviderContext } from "../define.ts"; +import { OPENAI_COMPATIBLE_HOST_COMPAT } from "../openai-compatible.ts"; +import { getEffortThinkingLevelMap } from "../reasoning.ts"; + +// Models StepFun has taken offline. Skip them even if models.dev still lists them. +const OFFLINE_MODEL_IDS = new Set(["step-1-32k", "step-2-16k"]); + +/** + * StepFun's Global (stepfun.ai) and China (stepfun.com) platforms. Reasoning + * uses DeepSeek's shape: a `thinking` toggle, top-level `reasoning_effort` with + * per-model values, and `reasoning_content` that must be sent back. + */ +export function stepfunModels(ctx: ProviderContext, source: string, baseUrl: string) { + return modelsDevModels(ctx, source, { + api: "openai-completions", + baseUrl, + recordReasoning: false, + include: (model) => !OFFLINE_MODEL_IDS.has(model.id), + map: (model, source): Model<"openai-completions"> => { + const thinkingLevelMap = getEffortThinkingLevelMap(source.reasoningOptions ?? []); + return { + ...model, + ...(thinkingLevelMap ? { thinkingLevelMap } : {}), + compat: { supportsReasoningEffort: thinkingLevelMap !== undefined }, + }; + }, + }); +} + +export const STEPFUN_COMPLETIONS_COMPAT = { + ...OPENAI_COMPATIBLE_HOST_COMPAT, + thinkingFormat: "deepseek", + requiresReasoningContentOnAssistantMessages: true, +} as const; + +export default defineProvider({ + id: "stepfun", + models: (ctx) => stepfunModels(ctx, "stepfun-ai", "https://api.stepfun.ai/v1"), + compat: { "openai-completions": STEPFUN_COMPLETIONS_COMPAT }, +}); diff --git a/packages/ai/src/env-api-keys.ts b/packages/ai/src/env-api-keys.ts index 84d69bf10..d19d8be1d 100644 --- a/packages/ai/src/env-api-keys.ts +++ b/packages/ai/src/env-api-keys.ts @@ -120,6 +120,14 @@ function getApiKeyEnvVars(provider: string): readonly string[] | undefined { "xiaomi-token-plan-cn": "XIAOMI_TOKEN_PLAN_CN_API_KEY", "xiaomi-token-plan-ams": "XIAOMI_TOKEN_PLAN_AMS_API_KEY", "xiaomi-token-plan-sgp": "XIAOMI_TOKEN_PLAN_SGP_API_KEY", + deepinfra: "DEEPINFRA_API_KEY", + nebius: "NEBIUS_API_KEY", + "ollama-cloud": "OLLAMA_API_KEY", + scaleway: "SCALEWAY_API_KEY", + siliconflow: "SILICONFLOW_API_KEY", + "siliconflow-cn": "SILICONFLOW_CN_API_KEY", + stepfun: "STEPFUN_API_KEY", + "stepfun-cn": "STEPFUN_CN_API_KEY", alibaba: "DASHSCOPE_API_KEY", "alibaba-cn": "DASHSCOPE_CN_API_KEY", "zai-api": "ZAI_PAYG_API_KEY", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 0c19311fd..6dd36edc2 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -11,6 +11,7 @@ import { BASETEN_CLASSIFIER_MODELS, BASETEN_IMAGE_MODELS, BASETEN_MODELS } from import { CEREBRAS_CLASSIFIER_MODELS, CEREBRAS_IMAGE_MODELS, CEREBRAS_MODELS } from "./providers/cerebras.models.ts"; import { CLOUDFLARE_AI_GATEWAY_CLASSIFIER_MODELS, CLOUDFLARE_AI_GATEWAY_IMAGE_MODELS, CLOUDFLARE_AI_GATEWAY_MODELS } from "./providers/cloudflare-ai-gateway.models.ts"; import { CLOUDFLARE_WORKERS_AI_CLASSIFIER_MODELS, CLOUDFLARE_WORKERS_AI_IMAGE_MODELS, CLOUDFLARE_WORKERS_AI_MODELS } from "./providers/cloudflare-workers-ai.models.ts"; +import { DEEPINFRA_CLASSIFIER_MODELS, DEEPINFRA_IMAGE_MODELS, DEEPINFRA_MODELS } from "./providers/deepinfra.models.ts"; import { DEEPSEEK_CLASSIFIER_MODELS, DEEPSEEK_IMAGE_MODELS, DEEPSEEK_MODELS } from "./providers/deepseek.models.ts"; import { FIREWORKS_CLASSIFIER_MODELS, FIREWORKS_IMAGE_MODELS, FIREWORKS_MODELS } from "./providers/fireworks.models.ts"; import { GITHUB_COPILOT_CLASSIFIER_MODELS, GITHUB_COPILOT_IMAGE_MODELS, GITHUB_COPILOT_MODELS } from "./providers/github-copilot.models.ts"; @@ -25,7 +26,9 @@ import { MINIMAX_CN_CLASSIFIER_MODELS, MINIMAX_CN_IMAGE_MODELS, MINIMAX_CN_MODEL import { MISTRAL_CLASSIFIER_MODELS, MISTRAL_IMAGE_MODELS, MISTRAL_MODELS } from "./providers/mistral.models.ts"; import { MOONSHOTAI_CLASSIFIER_MODELS, MOONSHOTAI_IMAGE_MODELS, MOONSHOTAI_MODELS } from "./providers/moonshotai.models.ts"; import { MOONSHOTAI_CN_CLASSIFIER_MODELS, MOONSHOTAI_CN_IMAGE_MODELS, MOONSHOTAI_CN_MODELS } from "./providers/moonshotai-cn.models.ts"; +import { NEBIUS_CLASSIFIER_MODELS, NEBIUS_IMAGE_MODELS, NEBIUS_MODELS } from "./providers/nebius.models.ts"; import { NVIDIA_CLASSIFIER_MODELS, NVIDIA_IMAGE_MODELS, NVIDIA_MODELS } from "./providers/nvidia.models.ts"; +import { OLLAMA_CLOUD_CLASSIFIER_MODELS, OLLAMA_CLOUD_IMAGE_MODELS, OLLAMA_CLOUD_MODELS } from "./providers/ollama-cloud.models.ts"; import { OPENAI_CLASSIFIER_MODELS, OPENAI_IMAGE_MODELS, OPENAI_MODELS } from "./providers/openai.models.ts"; import { OPENAI_CODEX_CLASSIFIER_MODELS, OPENAI_CODEX_IMAGE_MODELS, OPENAI_CODEX_MODELS } from "./providers/openai-codex.models.ts"; import { OPENCODE_CLASSIFIER_MODELS, OPENCODE_IMAGE_MODELS, OPENCODE_MODELS } from "./providers/opencode.models.ts"; @@ -35,6 +38,11 @@ import { QWEN_TOKEN_PLAN_CLASSIFIER_MODELS, QWEN_TOKEN_PLAN_IMAGE_MODELS, QWEN_T import { QWEN_TOKEN_PLAN_CN_CLASSIFIER_MODELS, QWEN_TOKEN_PLAN_CN_IMAGE_MODELS, QWEN_TOKEN_PLAN_CN_MODELS } from "./providers/qwen-token-plan-cn.models.ts"; import { QWEN_TOKEN_PLAN_INDIVIDUAL_CLASSIFIER_MODELS, QWEN_TOKEN_PLAN_INDIVIDUAL_IMAGE_MODELS, QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS } from "./providers/qwen-token-plan-individual.models.ts"; import { RADIUS_CLASSIFIER_MODELS, RADIUS_IMAGE_MODELS, RADIUS_MODELS } from "./providers/radius.models.ts"; +import { SCALEWAY_CLASSIFIER_MODELS, SCALEWAY_IMAGE_MODELS, SCALEWAY_MODELS } from "./providers/scaleway.models.ts"; +import { SILICONFLOW_CLASSIFIER_MODELS, SILICONFLOW_IMAGE_MODELS, SILICONFLOW_MODELS } from "./providers/siliconflow.models.ts"; +import { SILICONFLOW_CN_CLASSIFIER_MODELS, SILICONFLOW_CN_IMAGE_MODELS, SILICONFLOW_CN_MODELS } from "./providers/siliconflow-cn.models.ts"; +import { STEPFUN_CLASSIFIER_MODELS, STEPFUN_IMAGE_MODELS, STEPFUN_MODELS } from "./providers/stepfun.models.ts"; +import { STEPFUN_CN_CLASSIFIER_MODELS, STEPFUN_CN_IMAGE_MODELS, STEPFUN_CN_MODELS } from "./providers/stepfun-cn.models.ts"; import { TOGETHER_CLASSIFIER_MODELS, TOGETHER_IMAGE_MODELS, TOGETHER_MODELS } from "./providers/together.models.ts"; import { TYPESAFE_CLASSIFIER_MODELS, TYPESAFE_IMAGE_MODELS, TYPESAFE_MODELS } from "./providers/typesafe.models.ts"; import { VERCEL_AI_GATEWAY_CLASSIFIER_MODELS, VERCEL_AI_GATEWAY_IMAGE_MODELS, VERCEL_AI_GATEWAY_MODELS } from "./providers/vercel-ai-gateway.models.ts"; @@ -59,6 +67,7 @@ export const MODELS: { readonly "cerebras": typeof CEREBRAS_MODELS; readonly "cloudflare-ai-gateway": typeof CLOUDFLARE_AI_GATEWAY_MODELS; readonly "cloudflare-workers-ai": typeof CLOUDFLARE_WORKERS_AI_MODELS; + readonly "deepinfra": typeof DEEPINFRA_MODELS; readonly "deepseek": typeof DEEPSEEK_MODELS; readonly "fireworks": typeof FIREWORKS_MODELS; readonly "github-copilot": typeof GITHUB_COPILOT_MODELS; @@ -73,7 +82,9 @@ export const MODELS: { readonly "mistral": typeof MISTRAL_MODELS; readonly "moonshotai": typeof MOONSHOTAI_MODELS; readonly "moonshotai-cn": typeof MOONSHOTAI_CN_MODELS; + readonly "nebius": typeof NEBIUS_MODELS; readonly "nvidia": typeof NVIDIA_MODELS; + readonly "ollama-cloud": typeof OLLAMA_CLOUD_MODELS; readonly "openai": typeof OPENAI_MODELS; readonly "openai-codex": typeof OPENAI_CODEX_MODELS; readonly "opencode": typeof OPENCODE_MODELS; @@ -83,6 +94,11 @@ export const MODELS: { readonly "qwen-token-plan-cn": typeof QWEN_TOKEN_PLAN_CN_MODELS; readonly "qwen-token-plan-individual": typeof QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS; readonly "radius": typeof RADIUS_MODELS; + readonly "scaleway": typeof SCALEWAY_MODELS; + readonly "siliconflow": typeof SILICONFLOW_MODELS; + readonly "siliconflow-cn": typeof SILICONFLOW_CN_MODELS; + readonly "stepfun": typeof STEPFUN_MODELS; + readonly "stepfun-cn": typeof STEPFUN_CN_MODELS; readonly "together": typeof TOGETHER_MODELS; readonly "typesafe": typeof TYPESAFE_MODELS; readonly "vercel-ai-gateway": typeof VERCEL_AI_GATEWAY_MODELS; @@ -106,6 +122,7 @@ export const MODELS: { "cerebras": CEREBRAS_MODELS, "cloudflare-ai-gateway": CLOUDFLARE_AI_GATEWAY_MODELS, "cloudflare-workers-ai": CLOUDFLARE_WORKERS_AI_MODELS, + "deepinfra": DEEPINFRA_MODELS, "deepseek": DEEPSEEK_MODELS, "fireworks": FIREWORKS_MODELS, "github-copilot": GITHUB_COPILOT_MODELS, @@ -120,7 +137,9 @@ export const MODELS: { "mistral": MISTRAL_MODELS, "moonshotai": MOONSHOTAI_MODELS, "moonshotai-cn": MOONSHOTAI_CN_MODELS, + "nebius": NEBIUS_MODELS, "nvidia": NVIDIA_MODELS, + "ollama-cloud": OLLAMA_CLOUD_MODELS, "openai": OPENAI_MODELS, "openai-codex": OPENAI_CODEX_MODELS, "opencode": OPENCODE_MODELS, @@ -130,6 +149,11 @@ export const MODELS: { "qwen-token-plan-cn": QWEN_TOKEN_PLAN_CN_MODELS, "qwen-token-plan-individual": QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS, "radius": RADIUS_MODELS, + "scaleway": SCALEWAY_MODELS, + "siliconflow": SILICONFLOW_MODELS, + "siliconflow-cn": SILICONFLOW_CN_MODELS, + "stepfun": STEPFUN_MODELS, + "stepfun-cn": STEPFUN_CN_MODELS, "together": TOGETHER_MODELS, "typesafe": TYPESAFE_MODELS, "vercel-ai-gateway": VERCEL_AI_GATEWAY_MODELS, @@ -155,6 +179,7 @@ export const IMAGE_MODELS: { readonly "cerebras": typeof CEREBRAS_IMAGE_MODELS; readonly "cloudflare-ai-gateway": typeof CLOUDFLARE_AI_GATEWAY_IMAGE_MODELS; readonly "cloudflare-workers-ai": typeof CLOUDFLARE_WORKERS_AI_IMAGE_MODELS; + readonly "deepinfra": typeof DEEPINFRA_IMAGE_MODELS; readonly "deepseek": typeof DEEPSEEK_IMAGE_MODELS; readonly "fireworks": typeof FIREWORKS_IMAGE_MODELS; readonly "github-copilot": typeof GITHUB_COPILOT_IMAGE_MODELS; @@ -169,7 +194,9 @@ export const IMAGE_MODELS: { readonly "mistral": typeof MISTRAL_IMAGE_MODELS; readonly "moonshotai": typeof MOONSHOTAI_IMAGE_MODELS; readonly "moonshotai-cn": typeof MOONSHOTAI_CN_IMAGE_MODELS; + readonly "nebius": typeof NEBIUS_IMAGE_MODELS; readonly "nvidia": typeof NVIDIA_IMAGE_MODELS; + readonly "ollama-cloud": typeof OLLAMA_CLOUD_IMAGE_MODELS; readonly "openai": typeof OPENAI_IMAGE_MODELS; readonly "openai-codex": typeof OPENAI_CODEX_IMAGE_MODELS; readonly "opencode": typeof OPENCODE_IMAGE_MODELS; @@ -179,6 +206,11 @@ export const IMAGE_MODELS: { readonly "qwen-token-plan-cn": typeof QWEN_TOKEN_PLAN_CN_IMAGE_MODELS; readonly "qwen-token-plan-individual": typeof QWEN_TOKEN_PLAN_INDIVIDUAL_IMAGE_MODELS; readonly "radius": typeof RADIUS_IMAGE_MODELS; + readonly "scaleway": typeof SCALEWAY_IMAGE_MODELS; + readonly "siliconflow": typeof SILICONFLOW_IMAGE_MODELS; + readonly "siliconflow-cn": typeof SILICONFLOW_CN_IMAGE_MODELS; + readonly "stepfun": typeof STEPFUN_IMAGE_MODELS; + readonly "stepfun-cn": typeof STEPFUN_CN_IMAGE_MODELS; readonly "together": typeof TOGETHER_IMAGE_MODELS; readonly "typesafe": typeof TYPESAFE_IMAGE_MODELS; readonly "vercel-ai-gateway": typeof VERCEL_AI_GATEWAY_IMAGE_MODELS; @@ -202,6 +234,7 @@ export const IMAGE_MODELS: { "cerebras": CEREBRAS_IMAGE_MODELS, "cloudflare-ai-gateway": CLOUDFLARE_AI_GATEWAY_IMAGE_MODELS, "cloudflare-workers-ai": CLOUDFLARE_WORKERS_AI_IMAGE_MODELS, + "deepinfra": DEEPINFRA_IMAGE_MODELS, "deepseek": DEEPSEEK_IMAGE_MODELS, "fireworks": FIREWORKS_IMAGE_MODELS, "github-copilot": GITHUB_COPILOT_IMAGE_MODELS, @@ -216,7 +249,9 @@ export const IMAGE_MODELS: { "mistral": MISTRAL_IMAGE_MODELS, "moonshotai": MOONSHOTAI_IMAGE_MODELS, "moonshotai-cn": MOONSHOTAI_CN_IMAGE_MODELS, + "nebius": NEBIUS_IMAGE_MODELS, "nvidia": NVIDIA_IMAGE_MODELS, + "ollama-cloud": OLLAMA_CLOUD_IMAGE_MODELS, "openai": OPENAI_IMAGE_MODELS, "openai-codex": OPENAI_CODEX_IMAGE_MODELS, "opencode": OPENCODE_IMAGE_MODELS, @@ -226,6 +261,11 @@ export const IMAGE_MODELS: { "qwen-token-plan-cn": QWEN_TOKEN_PLAN_CN_IMAGE_MODELS, "qwen-token-plan-individual": QWEN_TOKEN_PLAN_INDIVIDUAL_IMAGE_MODELS, "radius": RADIUS_IMAGE_MODELS, + "scaleway": SCALEWAY_IMAGE_MODELS, + "siliconflow": SILICONFLOW_IMAGE_MODELS, + "siliconflow-cn": SILICONFLOW_CN_IMAGE_MODELS, + "stepfun": STEPFUN_IMAGE_MODELS, + "stepfun-cn": STEPFUN_CN_IMAGE_MODELS, "together": TOGETHER_IMAGE_MODELS, "typesafe": TYPESAFE_IMAGE_MODELS, "vercel-ai-gateway": VERCEL_AI_GATEWAY_IMAGE_MODELS, @@ -251,6 +291,7 @@ export const CLASSIFIER_MODELS: { readonly "cerebras": typeof CEREBRAS_CLASSIFIER_MODELS; readonly "cloudflare-ai-gateway": typeof CLOUDFLARE_AI_GATEWAY_CLASSIFIER_MODELS; readonly "cloudflare-workers-ai": typeof CLOUDFLARE_WORKERS_AI_CLASSIFIER_MODELS; + readonly "deepinfra": typeof DEEPINFRA_CLASSIFIER_MODELS; readonly "deepseek": typeof DEEPSEEK_CLASSIFIER_MODELS; readonly "fireworks": typeof FIREWORKS_CLASSIFIER_MODELS; readonly "github-copilot": typeof GITHUB_COPILOT_CLASSIFIER_MODELS; @@ -265,7 +306,9 @@ export const CLASSIFIER_MODELS: { readonly "mistral": typeof MISTRAL_CLASSIFIER_MODELS; readonly "moonshotai": typeof MOONSHOTAI_CLASSIFIER_MODELS; readonly "moonshotai-cn": typeof MOONSHOTAI_CN_CLASSIFIER_MODELS; + readonly "nebius": typeof NEBIUS_CLASSIFIER_MODELS; readonly "nvidia": typeof NVIDIA_CLASSIFIER_MODELS; + readonly "ollama-cloud": typeof OLLAMA_CLOUD_CLASSIFIER_MODELS; readonly "openai": typeof OPENAI_CLASSIFIER_MODELS; readonly "openai-codex": typeof OPENAI_CODEX_CLASSIFIER_MODELS; readonly "opencode": typeof OPENCODE_CLASSIFIER_MODELS; @@ -275,6 +318,11 @@ export const CLASSIFIER_MODELS: { readonly "qwen-token-plan-cn": typeof QWEN_TOKEN_PLAN_CN_CLASSIFIER_MODELS; readonly "qwen-token-plan-individual": typeof QWEN_TOKEN_PLAN_INDIVIDUAL_CLASSIFIER_MODELS; readonly "radius": typeof RADIUS_CLASSIFIER_MODELS; + readonly "scaleway": typeof SCALEWAY_CLASSIFIER_MODELS; + readonly "siliconflow": typeof SILICONFLOW_CLASSIFIER_MODELS; + readonly "siliconflow-cn": typeof SILICONFLOW_CN_CLASSIFIER_MODELS; + readonly "stepfun": typeof STEPFUN_CLASSIFIER_MODELS; + readonly "stepfun-cn": typeof STEPFUN_CN_CLASSIFIER_MODELS; readonly "together": typeof TOGETHER_CLASSIFIER_MODELS; readonly "typesafe": typeof TYPESAFE_CLASSIFIER_MODELS; readonly "vercel-ai-gateway": typeof VERCEL_AI_GATEWAY_CLASSIFIER_MODELS; @@ -298,6 +346,7 @@ export const CLASSIFIER_MODELS: { "cerebras": CEREBRAS_CLASSIFIER_MODELS, "cloudflare-ai-gateway": CLOUDFLARE_AI_GATEWAY_CLASSIFIER_MODELS, "cloudflare-workers-ai": CLOUDFLARE_WORKERS_AI_CLASSIFIER_MODELS, + "deepinfra": DEEPINFRA_CLASSIFIER_MODELS, "deepseek": DEEPSEEK_CLASSIFIER_MODELS, "fireworks": FIREWORKS_CLASSIFIER_MODELS, "github-copilot": GITHUB_COPILOT_CLASSIFIER_MODELS, @@ -312,7 +361,9 @@ export const CLASSIFIER_MODELS: { "mistral": MISTRAL_CLASSIFIER_MODELS, "moonshotai": MOONSHOTAI_CLASSIFIER_MODELS, "moonshotai-cn": MOONSHOTAI_CN_CLASSIFIER_MODELS, + "nebius": NEBIUS_CLASSIFIER_MODELS, "nvidia": NVIDIA_CLASSIFIER_MODELS, + "ollama-cloud": OLLAMA_CLOUD_CLASSIFIER_MODELS, "openai": OPENAI_CLASSIFIER_MODELS, "openai-codex": OPENAI_CODEX_CLASSIFIER_MODELS, "opencode": OPENCODE_CLASSIFIER_MODELS, @@ -322,6 +373,11 @@ export const CLASSIFIER_MODELS: { "qwen-token-plan-cn": QWEN_TOKEN_PLAN_CN_CLASSIFIER_MODELS, "qwen-token-plan-individual": QWEN_TOKEN_PLAN_INDIVIDUAL_CLASSIFIER_MODELS, "radius": RADIUS_CLASSIFIER_MODELS, + "scaleway": SCALEWAY_CLASSIFIER_MODELS, + "siliconflow": SILICONFLOW_CLASSIFIER_MODELS, + "siliconflow-cn": SILICONFLOW_CN_CLASSIFIER_MODELS, + "stepfun": STEPFUN_CLASSIFIER_MODELS, + "stepfun-cn": STEPFUN_CN_CLASSIFIER_MODELS, "together": TOGETHER_CLASSIFIER_MODELS, "typesafe": TYPESAFE_CLASSIFIER_MODELS, "vercel-ai-gateway": VERCEL_AI_GATEWAY_CLASSIFIER_MODELS, diff --git a/packages/ai/src/providers/all.ts b/packages/ai/src/providers/all.ts index 087002c38..cbf5404b8 100644 --- a/packages/ai/src/providers/all.ts +++ b/packages/ai/src/providers/all.ts @@ -12,6 +12,7 @@ import { cerebrasProvider } from "./cerebras.ts"; import { cloudflareAIGatewayProvider } from "./cloudflare-ai-gateway.ts"; import { cloudflareWorkersAIProvider } from "./cloudflare-workers-ai.ts"; import modelDataManifest from "./data/.manifest.json" with { type: "json" }; +import { deepinfraProvider } from "./deepinfra.ts"; import { deepseekProvider } from "./deepseek.ts"; import { fireworksProvider } from "./fireworks.ts"; import { githubCopilotProvider } from "./github-copilot.ts"; @@ -26,7 +27,9 @@ import { minimaxCnProvider } from "./minimax-cn.ts"; import { mistralProvider } from "./mistral.ts"; import { moonshotaiProvider } from "./moonshotai.ts"; import { moonshotaiCnProvider } from "./moonshotai-cn.ts"; +import { nebiusProvider } from "./nebius.ts"; import { nvidiaProvider } from "./nvidia.ts"; +import { ollamaCloudProvider } from "./ollama-cloud.ts"; import { openaiProvider } from "./openai.ts"; import { openaiCodexProvider } from "./openai-codex.ts"; import { opencodeProvider } from "./opencode.ts"; @@ -36,6 +39,11 @@ import { qwenTokenPlanProvider } from "./qwen-token-plan.ts"; import { qwenTokenPlanCnProvider } from "./qwen-token-plan-cn.ts"; import { qwenTokenPlanIndividualProvider } from "./qwen-token-plan-individual.ts"; import { radiusProvider } from "./radius.ts"; +import { scalewayProvider } from "./scaleway.ts"; +import { siliconflowProvider } from "./siliconflow.ts"; +import { siliconflowCnProvider } from "./siliconflow-cn.ts"; +import { stepfunProvider } from "./stepfun.ts"; +import { stepfunCnProvider } from "./stepfun-cn.ts"; import { togetherProvider } from "./together.ts"; import { typesafeProvider } from "./typesafe.ts"; import { vercelAIGatewayProvider } from "./vercel-ai-gateway.ts"; @@ -149,6 +157,7 @@ export function builtinProviders(): Provider[] { cerebrasProvider(), cloudflareAIGatewayProvider(), cloudflareWorkersAIProvider(), + deepinfraProvider(), deepseekProvider(), fireworksProvider(), githubCopilotProvider(), @@ -163,7 +172,9 @@ export function builtinProviders(): Provider[] { mistralProvider(), moonshotaiProvider(), moonshotaiCnProvider(), + nebiusProvider(), nvidiaProvider(), + ollamaCloudProvider(), openaiProvider(), openaiCodexProvider(), opencodeProvider(), @@ -173,6 +184,11 @@ export function builtinProviders(): Provider[] { qwenTokenPlanCnProvider(), qwenTokenPlanIndividualProvider(), radiusProvider(), + scalewayProvider(), + siliconflowProvider(), + siliconflowCnProvider(), + stepfunProvider(), + stepfunCnProvider(), togetherProvider(), typesafeProvider(), vercelAIGatewayProvider(), diff --git a/packages/ai/src/providers/deepinfra.models.ts b/packages/ai/src/providers/deepinfra.models.ts new file mode 100644 index 000000000..50eb552de --- /dev/null +++ b/packages/ai/src/providers/deepinfra.models.ts @@ -0,0 +1,14 @@ +// This file is auto-generated by catalog/generate.ts +// Do not edit manually - run 'npm run generate-models' to update + +import values from "./data/deepinfra.json" with { type: "json" }; +import { flattenChatModelCatalog, flattenClassifierModelCatalog, flattenImageModelCatalog, type ChatModelCatalog, type ClassifierModelCatalog, type ImageModelCatalog } from "../model-catalog.ts"; + +export const DEEPINFRA_MODELS: ChatModelCatalog = + flattenChatModelCatalog("deepinfra", values); + +export const DEEPINFRA_IMAGE_MODELS: ImageModelCatalog = + flattenImageModelCatalog("deepinfra", values); + +export const DEEPINFRA_CLASSIFIER_MODELS: ClassifierModelCatalog = + flattenClassifierModelCatalog("deepinfra", values); diff --git a/packages/ai/src/providers/deepinfra.ts b/packages/ai/src/providers/deepinfra.ts new file mode 100644 index 000000000..96200602e --- /dev/null +++ b/packages/ai/src/providers/deepinfra.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { DEEPINFRA_MODELS } from "./deepinfra.models.ts"; + +export function deepinfraProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "deepinfra", + name: "DeepInfra", + baseUrl: "https://api.deepinfra.com/v1/openai", + auth: { apiKey: envApiKeyAuth("DeepInfra API key", ["DEEPINFRA_API_KEY"]) }, + models: Object.values(DEEPINFRA_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/nebius.models.ts b/packages/ai/src/providers/nebius.models.ts new file mode 100644 index 000000000..53f6d8295 --- /dev/null +++ b/packages/ai/src/providers/nebius.models.ts @@ -0,0 +1,14 @@ +// This file is auto-generated by catalog/generate.ts +// Do not edit manually - run 'npm run generate-models' to update + +import values from "./data/nebius.json" with { type: "json" }; +import { flattenChatModelCatalog, flattenClassifierModelCatalog, flattenImageModelCatalog, type ChatModelCatalog, type ClassifierModelCatalog, type ImageModelCatalog } from "../model-catalog.ts"; + +export const NEBIUS_MODELS: ChatModelCatalog = + flattenChatModelCatalog("nebius", values); + +export const NEBIUS_IMAGE_MODELS: ImageModelCatalog = + flattenImageModelCatalog("nebius", values); + +export const NEBIUS_CLASSIFIER_MODELS: ClassifierModelCatalog = + flattenClassifierModelCatalog("nebius", values); diff --git a/packages/ai/src/providers/nebius.ts b/packages/ai/src/providers/nebius.ts new file mode 100644 index 000000000..8b80a82dd --- /dev/null +++ b/packages/ai/src/providers/nebius.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { NEBIUS_MODELS } from "./nebius.models.ts"; + +export function nebiusProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "nebius", + name: "Nebius Token Factory", + baseUrl: "https://api.tokenfactory.nebius.com/v1", + auth: { apiKey: envApiKeyAuth("Nebius Token Factory API key", ["NEBIUS_API_KEY"]) }, + models: Object.values(NEBIUS_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/ollama-cloud.models.ts b/packages/ai/src/providers/ollama-cloud.models.ts new file mode 100644 index 000000000..7c67ad35c --- /dev/null +++ b/packages/ai/src/providers/ollama-cloud.models.ts @@ -0,0 +1,14 @@ +// This file is auto-generated by catalog/generate.ts +// Do not edit manually - run 'npm run generate-models' to update + +import values from "./data/ollama-cloud.json" with { type: "json" }; +import { flattenChatModelCatalog, flattenClassifierModelCatalog, flattenImageModelCatalog, type ChatModelCatalog, type ClassifierModelCatalog, type ImageModelCatalog } from "../model-catalog.ts"; + +export const OLLAMA_CLOUD_MODELS: ChatModelCatalog = + flattenChatModelCatalog("ollama-cloud", values); + +export const OLLAMA_CLOUD_IMAGE_MODELS: ImageModelCatalog = + flattenImageModelCatalog("ollama-cloud", values); + +export const OLLAMA_CLOUD_CLASSIFIER_MODELS: ClassifierModelCatalog = + flattenClassifierModelCatalog("ollama-cloud", values); diff --git a/packages/ai/src/providers/ollama-cloud.ts b/packages/ai/src/providers/ollama-cloud.ts new file mode 100644 index 000000000..4c15b1839 --- /dev/null +++ b/packages/ai/src/providers/ollama-cloud.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { OLLAMA_CLOUD_MODELS } from "./ollama-cloud.models.ts"; + +export function ollamaCloudProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "ollama-cloud", + name: "Ollama Cloud", + baseUrl: "https://ollama.com/v1", + auth: { apiKey: envApiKeyAuth("Ollama Cloud API key", ["OLLAMA_API_KEY"]) }, + models: Object.values(OLLAMA_CLOUD_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/scaleway.models.ts b/packages/ai/src/providers/scaleway.models.ts new file mode 100644 index 000000000..7e9013b2e --- /dev/null +++ b/packages/ai/src/providers/scaleway.models.ts @@ -0,0 +1,14 @@ +// This file is auto-generated by catalog/generate.ts +// Do not edit manually - run 'npm run generate-models' to update + +import values from "./data/scaleway.json" with { type: "json" }; +import { flattenChatModelCatalog, flattenClassifierModelCatalog, flattenImageModelCatalog, type ChatModelCatalog, type ClassifierModelCatalog, type ImageModelCatalog } from "../model-catalog.ts"; + +export const SCALEWAY_MODELS: ChatModelCatalog = + flattenChatModelCatalog("scaleway", values); + +export const SCALEWAY_IMAGE_MODELS: ImageModelCatalog = + flattenImageModelCatalog("scaleway", values); + +export const SCALEWAY_CLASSIFIER_MODELS: ClassifierModelCatalog = + flattenClassifierModelCatalog("scaleway", values); diff --git a/packages/ai/src/providers/scaleway.ts b/packages/ai/src/providers/scaleway.ts new file mode 100644 index 000000000..586ac95e9 --- /dev/null +++ b/packages/ai/src/providers/scaleway.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { SCALEWAY_MODELS } from "./scaleway.models.ts"; + +export function scalewayProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "scaleway", + name: "Scaleway", + baseUrl: "https://api.scaleway.ai/v1", + auth: { apiKey: envApiKeyAuth("Scaleway API key", ["SCALEWAY_API_KEY"]) }, + models: Object.values(SCALEWAY_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/siliconflow-cn.models.ts b/packages/ai/src/providers/siliconflow-cn.models.ts new file mode 100644 index 000000000..7ff9b0d1e --- /dev/null +++ b/packages/ai/src/providers/siliconflow-cn.models.ts @@ -0,0 +1,14 @@ +// This file is auto-generated by catalog/generate.ts +// Do not edit manually - run 'npm run generate-models' to update + +import values from "./data/siliconflow-cn.json" with { type: "json" }; +import { flattenChatModelCatalog, flattenClassifierModelCatalog, flattenImageModelCatalog, type ChatModelCatalog, type ClassifierModelCatalog, type ImageModelCatalog } from "../model-catalog.ts"; + +export const SILICONFLOW_CN_MODELS: ChatModelCatalog = + flattenChatModelCatalog("siliconflow-cn", values); + +export const SILICONFLOW_CN_IMAGE_MODELS: ImageModelCatalog = + flattenImageModelCatalog("siliconflow-cn", values); + +export const SILICONFLOW_CN_CLASSIFIER_MODELS: ClassifierModelCatalog = + flattenClassifierModelCatalog("siliconflow-cn", values); diff --git a/packages/ai/src/providers/siliconflow-cn.ts b/packages/ai/src/providers/siliconflow-cn.ts new file mode 100644 index 000000000..9f936be31 --- /dev/null +++ b/packages/ai/src/providers/siliconflow-cn.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { SILICONFLOW_CN_MODELS } from "./siliconflow-cn.models.ts"; + +export function siliconflowCnProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "siliconflow-cn", + name: "SiliconFlow CN", + baseUrl: "https://api.siliconflow.cn/v1", + auth: { apiKey: envApiKeyAuth("SiliconFlow CN API key", ["SILICONFLOW_CN_API_KEY"]) }, + models: Object.values(SILICONFLOW_CN_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/siliconflow.models.ts b/packages/ai/src/providers/siliconflow.models.ts new file mode 100644 index 000000000..d08b34aa3 --- /dev/null +++ b/packages/ai/src/providers/siliconflow.models.ts @@ -0,0 +1,14 @@ +// This file is auto-generated by catalog/generate.ts +// Do not edit manually - run 'npm run generate-models' to update + +import values from "./data/siliconflow.json" with { type: "json" }; +import { flattenChatModelCatalog, flattenClassifierModelCatalog, flattenImageModelCatalog, type ChatModelCatalog, type ClassifierModelCatalog, type ImageModelCatalog } from "../model-catalog.ts"; + +export const SILICONFLOW_MODELS: ChatModelCatalog = + flattenChatModelCatalog("siliconflow", values); + +export const SILICONFLOW_IMAGE_MODELS: ImageModelCatalog = + flattenImageModelCatalog("siliconflow", values); + +export const SILICONFLOW_CLASSIFIER_MODELS: ClassifierModelCatalog = + flattenClassifierModelCatalog("siliconflow", values); diff --git a/packages/ai/src/providers/siliconflow.ts b/packages/ai/src/providers/siliconflow.ts new file mode 100644 index 000000000..1f907bf66 --- /dev/null +++ b/packages/ai/src/providers/siliconflow.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { SILICONFLOW_MODELS } from "./siliconflow.models.ts"; + +export function siliconflowProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "siliconflow", + name: "SiliconFlow", + baseUrl: "https://api.siliconflow.com/v1", + auth: { apiKey: envApiKeyAuth("SiliconFlow API key", ["SILICONFLOW_API_KEY"]) }, + models: Object.values(SILICONFLOW_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/stepfun-cn.models.ts b/packages/ai/src/providers/stepfun-cn.models.ts new file mode 100644 index 000000000..5289a403e --- /dev/null +++ b/packages/ai/src/providers/stepfun-cn.models.ts @@ -0,0 +1,14 @@ +// This file is auto-generated by catalog/generate.ts +// Do not edit manually - run 'npm run generate-models' to update + +import values from "./data/stepfun-cn.json" with { type: "json" }; +import { flattenChatModelCatalog, flattenClassifierModelCatalog, flattenImageModelCatalog, type ChatModelCatalog, type ClassifierModelCatalog, type ImageModelCatalog } from "../model-catalog.ts"; + +export const STEPFUN_CN_MODELS: ChatModelCatalog = + flattenChatModelCatalog("stepfun-cn", values); + +export const STEPFUN_CN_IMAGE_MODELS: ImageModelCatalog = + flattenImageModelCatalog("stepfun-cn", values); + +export const STEPFUN_CN_CLASSIFIER_MODELS: ClassifierModelCatalog = + flattenClassifierModelCatalog("stepfun-cn", values); diff --git a/packages/ai/src/providers/stepfun-cn.ts b/packages/ai/src/providers/stepfun-cn.ts new file mode 100644 index 000000000..50aba1882 --- /dev/null +++ b/packages/ai/src/providers/stepfun-cn.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { STEPFUN_CN_MODELS } from "./stepfun-cn.models.ts"; + +export function stepfunCnProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "stepfun-cn", + name: "StepFun CN", + baseUrl: "https://api.stepfun.com/v1", + auth: { apiKey: envApiKeyAuth("StepFun CN API key", ["STEPFUN_CN_API_KEY"]) }, + models: Object.values(STEPFUN_CN_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/stepfun.models.ts b/packages/ai/src/providers/stepfun.models.ts new file mode 100644 index 000000000..ee9b688d7 --- /dev/null +++ b/packages/ai/src/providers/stepfun.models.ts @@ -0,0 +1,14 @@ +// This file is auto-generated by catalog/generate.ts +// Do not edit manually - run 'npm run generate-models' to update + +import values from "./data/stepfun.json" with { type: "json" }; +import { flattenChatModelCatalog, flattenClassifierModelCatalog, flattenImageModelCatalog, type ChatModelCatalog, type ClassifierModelCatalog, type ImageModelCatalog } from "../model-catalog.ts"; + +export const STEPFUN_MODELS: ChatModelCatalog = + flattenChatModelCatalog("stepfun", values); + +export const STEPFUN_IMAGE_MODELS: ImageModelCatalog = + flattenImageModelCatalog("stepfun", values); + +export const STEPFUN_CLASSIFIER_MODELS: ClassifierModelCatalog = + flattenClassifierModelCatalog("stepfun", values); diff --git a/packages/ai/src/providers/stepfun.ts b/packages/ai/src/providers/stepfun.ts new file mode 100644 index 000000000..7b712e99b --- /dev/null +++ b/packages/ai/src/providers/stepfun.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { STEPFUN_MODELS } from "./stepfun.models.ts"; + +export function stepfunProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "stepfun", + name: "StepFun", + baseUrl: "https://api.stepfun.ai/v1", + auth: { apiKey: envApiKeyAuth("StepFun API key", ["STEPFUN_API_KEY"]) }, + models: Object.values(STEPFUN_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 5bb4fbfb6..69044483f 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -133,7 +133,15 @@ export type KnownProvider = | "alibaba" | "alibaba-cn" | "zai-api" - | "zai-api-cn"; + | "zai-api-cn" + | "deepinfra" + | "nebius" + | "ollama-cloud" + | "scaleway" + | "siliconflow" + | "siliconflow-cn" + | "stepfun" + | "stepfun-cn"; export type ProviderId = KnownProvider | string; export type ToolChoice = "auto" | "none"; diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index 3fa5b9016..a11e76e50 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -345,6 +345,102 @@ describe("AI Providers Abort Tests", () => { }); }); + describe.skipIf(!process.env.DEEPINFRA_API_KEY)("DeepInfra Provider Abort", () => { + const llm = getModel("deepinfra", "openai/gpt-oss-120b"); + + it("should abort mid-stream", { retry: 3 }, async () => { + await testAbortSignal(llm); + }); + + it("should handle immediate abort", { retry: 3 }, async () => { + await testImmediateAbort(llm); + }); + }); + + describe.skipIf(!process.env.NEBIUS_API_KEY)("Nebius Token Factory Provider Abort", () => { + const llm = getModel("nebius", "openai/gpt-oss-120b"); + + it("should abort mid-stream", { retry: 3 }, async () => { + await testAbortSignal(llm); + }); + + it("should handle immediate abort", { retry: 3 }, async () => { + await testImmediateAbort(llm); + }); + }); + + describe.skipIf(!process.env.OLLAMA_API_KEY)("Ollama Cloud Provider Abort", () => { + const llm = getModel("ollama-cloud", "gpt-oss:120b"); + + it("should abort mid-stream", { retry: 3 }, async () => { + await testAbortSignal(llm); + }); + + it("should handle immediate abort", { retry: 3 }, async () => { + await testImmediateAbort(llm); + }); + }); + + describe.skipIf(!process.env.SCALEWAY_API_KEY)("Scaleway Generative APIs Provider Abort", () => { + const llm = getModel("scaleway", "gpt-oss-120b"); + + it("should abort mid-stream", { retry: 3 }, async () => { + await testAbortSignal(llm); + }); + + it("should handle immediate abort", { retry: 3 }, async () => { + await testImmediateAbort(llm); + }); + }); + + describe.skipIf(!process.env.SILICONFLOW_API_KEY)("SiliconFlow Provider Abort", () => { + const llm = getModel("siliconflow", "zai-org/GLM-5.2"); + + it("should abort mid-stream", { retry: 3 }, async () => { + await testAbortSignal(llm); + }); + + it("should handle immediate abort", { retry: 3 }, async () => { + await testImmediateAbort(llm); + }); + }); + + describe.skipIf(!process.env.SILICONFLOW_CN_API_KEY)("SiliconFlow (China) Provider Abort", () => { + const llm = getModel("siliconflow-cn", "zai-org/GLM-5.2"); + + it("should abort mid-stream", { retry: 3 }, async () => { + await testAbortSignal(llm); + }); + + it("should handle immediate abort", { retry: 3 }, async () => { + await testImmediateAbort(llm); + }); + }); + + describe.skipIf(!process.env.STEPFUN_API_KEY)("StepFun Provider Abort", () => { + const llm = getModel("stepfun", "step-3.7-flash"); + + it("should abort mid-stream", { retry: 3 }, async () => { + await testAbortSignal(llm); + }); + + it("should handle immediate abort", { retry: 3 }, async () => { + await testImmediateAbort(llm); + }); + }); + + describe.skipIf(!process.env.STEPFUN_CN_API_KEY)("StepFun (China) Provider Abort", () => { + const llm = getModel("stepfun-cn", "step-3.7-flash"); + + it("should abort mid-stream", { retry: 3 }, async () => { + await testAbortSignal(llm); + }); + + it("should handle immediate abort", { retry: 3 }, async () => { + await testImmediateAbort(llm); + }); + }); + describe.skipIf(!process.env.KIMI_API_KEY)("Kimi For Coding Provider Abort", () => { const llm = getModel("kimi-coding", "kimi-for-coding"); diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 0479812e5..b644db886 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -551,6 +551,102 @@ describe("Context overflow error handling", () => { }, 120000); }); + describe.skipIf(!process.env.DEEPINFRA_API_KEY)("DeepInfra", () => { + it("openai/gpt-oss-120b - should detect overflow via isContextOverflow", async () => { + const model = getModel("deepinfra", "openai/gpt-oss-120b"); + const result = await testContextOverflow(model, process.env.DEEPINFRA_API_KEY!); + logResult(result); + + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toMatch(/input length/i); + expect(isContextOverflow(result.response, model.contextWindow)).toBe(true); + }, 120000); + }); + + describe.skipIf(!process.env.NEBIUS_API_KEY)("Nebius Token Factory", () => { + it("openai/gpt-oss-120b - should detect overflow via isContextOverflow", async () => { + const model = getModel("nebius", "openai/gpt-oss-120b"); + const result = await testContextOverflow(model, process.env.NEBIUS_API_KEY!); + logResult(result); + + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toMatch(/input length/i); + expect(isContextOverflow(result.response, model.contextWindow)).toBe(true); + }, 120000); + }); + + describe.skipIf(!process.env.OLLAMA_API_KEY)("Ollama Cloud", () => { + it("gpt-oss:120b - should detect overflow via isContextOverflow", async () => { + const model = getModel("ollama-cloud", "gpt-oss:120b"); + const result = await testContextOverflow(model, process.env.OLLAMA_API_KEY!); + logResult(result); + + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toMatch(/input length/i); + expect(isContextOverflow(result.response, model.contextWindow)).toBe(true); + }, 120000); + }); + + describe.skipIf(!process.env.SCALEWAY_API_KEY)("Scaleway Generative APIs", () => { + it("gpt-oss-120b - should detect overflow via isContextOverflow", async () => { + const model = getModel("scaleway", "gpt-oss-120b"); + const result = await testContextOverflow(model, process.env.SCALEWAY_API_KEY!); + logResult(result); + + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toMatch(/input length/i); + expect(isContextOverflow(result.response, model.contextWindow)).toBe(true); + }, 120000); + }); + + describe.skipIf(!process.env.SILICONFLOW_API_KEY)("SiliconFlow", () => { + it("zai-org/GLM-5.2 - should detect overflow via isContextOverflow", async () => { + const model = getModel("siliconflow", "zai-org/GLM-5.2"); + const result = await testContextOverflow(model, process.env.SILICONFLOW_API_KEY!); + logResult(result); + + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toMatch(/input length/i); + expect(isContextOverflow(result.response, model.contextWindow)).toBe(true); + }, 120000); + }); + + describe.skipIf(!process.env.SILICONFLOW_CN_API_KEY)("SiliconFlow (China)", () => { + it("zai-org/GLM-5.2 - should detect overflow via isContextOverflow", async () => { + const model = getModel("siliconflow-cn", "zai-org/GLM-5.2"); + const result = await testContextOverflow(model, process.env.SILICONFLOW_CN_API_KEY!); + logResult(result); + + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toMatch(/input length/i); + expect(isContextOverflow(result.response, model.contextWindow)).toBe(true); + }, 120000); + }); + + describe.skipIf(!process.env.STEPFUN_API_KEY)("StepFun", () => { + it("step-3.7-flash - should detect overflow via isContextOverflow", async () => { + const model = getModel("stepfun", "step-3.7-flash"); + const result = await testContextOverflow(model, process.env.STEPFUN_API_KEY!); + logResult(result); + + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toMatch(/input length/i); + expect(isContextOverflow(result.response, model.contextWindow)).toBe(true); + }, 120000); + }); + + describe.skipIf(!process.env.STEPFUN_CN_API_KEY)("StepFun (China)", () => { + it("step-3.7-flash - should detect overflow via isContextOverflow", async () => { + const model = getModel("stepfun-cn", "step-3.7-flash"); + const result = await testContextOverflow(model, process.env.STEPFUN_CN_API_KEY!); + logResult(result); + + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toMatch(/input length/i); + expect(isContextOverflow(result.response, model.contextWindow)).toBe(true); + }, 120000); + }); + // ============================================================================= // Kimi For Coding // ============================================================================= diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index da1f426ef..3f3351065 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -141,6 +141,14 @@ const PROVIDER_MODEL_PAIRS: ProviderModelPair[] = [ { provider: "alibaba-cn", model: "qwen3.7-max", label: "alibaba-cn-qwen3.7-max" }, { provider: "zai-api", model: "glm-5.3", label: "zai-api-glm-5.3" }, { provider: "zai-api-cn", model: "glm-5.3", label: "zai-api-cn-glm-5.3" }, + { provider: "deepinfra", model: "openai/gpt-oss-120b", label: "deepinfra-openai-gpt-oss-120b" }, + { provider: "nebius", model: "openai/gpt-oss-120b", label: "nebius-openai-gpt-oss-120b" }, + { provider: "ollama-cloud", model: "gpt-oss:120b", label: "ollama-cloud-gpt-oss:120b" }, + { provider: "scaleway", model: "gpt-oss-120b", label: "scaleway-gpt-oss-120b" }, + { provider: "siliconflow", model: "zai-org/GLM-5.2", label: "siliconflow-zai-org-GLM-5.2" }, + { provider: "siliconflow-cn", model: "zai-org/GLM-5.2", label: "siliconflow-cn-zai-org-GLM-5.2" }, + { provider: "stepfun", model: "step-3.7-flash", label: "stepfun-step-3.7-flash" }, + { provider: "stepfun-cn", model: "step-3.7-flash", label: "stepfun-cn-step-3.7-flash" }, { provider: "qwen-token-plan-individual", model: "qwen3.8-max", diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index 6dcdf9f9e..6e260e079 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -699,6 +699,166 @@ describe("AI Providers Empty Message Tests", () => { }); }); + describe.skipIf(!process.env.DEEPINFRA_API_KEY)("DeepInfra Provider Empty Messages", () => { + const llm = getModel("deepinfra", "openai/gpt-oss-120b"); + + it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { + await testEmptyMessage(llm); + }); + + it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => { + await testEmptyStringMessage(llm); + }); + + it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { + await testWhitespaceOnlyMessage(llm); + }); + + it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { + await testEmptyAssistantMessage(llm); + }); + }); + + describe.skipIf(!process.env.NEBIUS_API_KEY)("Nebius Token Factory Provider Empty Messages", () => { + const llm = getModel("nebius", "openai/gpt-oss-120b"); + + it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { + await testEmptyMessage(llm); + }); + + it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => { + await testEmptyStringMessage(llm); + }); + + it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { + await testWhitespaceOnlyMessage(llm); + }); + + it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { + await testEmptyAssistantMessage(llm); + }); + }); + + describe.skipIf(!process.env.OLLAMA_API_KEY)("Ollama Cloud Provider Empty Messages", () => { + const llm = getModel("ollama-cloud", "gpt-oss:120b"); + + it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { + await testEmptyMessage(llm); + }); + + it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => { + await testEmptyStringMessage(llm); + }); + + it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { + await testWhitespaceOnlyMessage(llm); + }); + + it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { + await testEmptyAssistantMessage(llm); + }); + }); + + describe.skipIf(!process.env.SCALEWAY_API_KEY)("Scaleway Generative APIs Provider Empty Messages", () => { + const llm = getModel("scaleway", "gpt-oss-120b"); + + it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { + await testEmptyMessage(llm); + }); + + it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => { + await testEmptyStringMessage(llm); + }); + + it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { + await testWhitespaceOnlyMessage(llm); + }); + + it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { + await testEmptyAssistantMessage(llm); + }); + }); + + describe.skipIf(!process.env.SILICONFLOW_API_KEY)("SiliconFlow Provider Empty Messages", () => { + const llm = getModel("siliconflow", "zai-org/GLM-5.2"); + + it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { + await testEmptyMessage(llm); + }); + + it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => { + await testEmptyStringMessage(llm); + }); + + it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { + await testWhitespaceOnlyMessage(llm); + }); + + it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { + await testEmptyAssistantMessage(llm); + }); + }); + + describe.skipIf(!process.env.SILICONFLOW_CN_API_KEY)("SiliconFlow (China) Provider Empty Messages", () => { + const llm = getModel("siliconflow-cn", "zai-org/GLM-5.2"); + + it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { + await testEmptyMessage(llm); + }); + + it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => { + await testEmptyStringMessage(llm); + }); + + it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { + await testWhitespaceOnlyMessage(llm); + }); + + it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { + await testEmptyAssistantMessage(llm); + }); + }); + + describe.skipIf(!process.env.STEPFUN_API_KEY)("StepFun Provider Empty Messages", () => { + const llm = getModel("stepfun", "step-3.7-flash"); + + it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { + await testEmptyMessage(llm); + }); + + it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => { + await testEmptyStringMessage(llm); + }); + + it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { + await testWhitespaceOnlyMessage(llm); + }); + + it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { + await testEmptyAssistantMessage(llm); + }); + }); + + describe.skipIf(!process.env.STEPFUN_CN_API_KEY)("StepFun (China) Provider Empty Messages", () => { + const llm = getModel("stepfun-cn", "step-3.7-flash"); + + it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { + await testEmptyMessage(llm); + }); + + it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => { + await testEmptyStringMessage(llm); + }); + + it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { + await testWhitespaceOnlyMessage(llm); + }); + + it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { + await testEmptyAssistantMessage(llm); + }); + }); + describe.skipIf(!process.env.KIMI_API_KEY)("Kimi For Coding Provider Empty Messages", () => { const llm = getModel("kimi-coding", "kimi-for-coding"); diff --git a/packages/ai/test/hosted-provider-models.test.ts b/packages/ai/test/hosted-provider-models.test.ts new file mode 100644 index 000000000..bd16811eb --- /dev/null +++ b/packages/ai/test/hosted-provider-models.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import { hasApi } from "../src/models.ts"; +import { getBuiltinModel, getBuiltinModels } from "../src/providers/all.ts"; +import type { Model } from "../src/types.ts"; + +const HOSTS = [ + ["deepinfra", "https://api.deepinfra.com/v1/openai"], + ["nebius", "https://api.tokenfactory.nebius.com/v1"], + ["ollama-cloud", "https://ollama.com/v1"], + ["scaleway", "https://api.scaleway.ai/v1"], + ["siliconflow", "https://api.siliconflow.com/v1"], + ["siliconflow-cn", "https://api.siliconflow.cn/v1"], + ["stepfun", "https://api.stepfun.ai/v1"], + ["stepfun-cn", "https://api.stepfun.com/v1"], +] as const; + +function completionsModels(provider: (typeof HOSTS)[number][0]): Model<"openai-completions">[] { + return getBuiltinModels(provider).filter((model) => hasApi(model, "openai-completions")); +} + +describe("hosted OpenAI-compatible providers", () => { + it.each(HOSTS)("serves %s through Chat Completions without OpenAI-only request features", (provider, baseUrl) => { + const models = getBuiltinModels(provider); + expect(models.length).toBeGreaterThan(0); + for (const model of models) { + expect(model).toMatchObject({ provider, api: "openai-completions", baseUrl }); + expect(model.compat).toMatchObject({ + supportsStore: false, + supportsDeveloperRole: false, + supportsLongCacheRetention: false, + }); + } + }); + + it.each(HOSTS)("sends reasoning_effort on %s only for models with cataloged effort levels", (provider) => { + for (const model of completionsModels(provider)) { + if (!model.reasoning) continue; + if (model.compat?.supportsReasoningEffort === false) { + expect(model.thinkingLevelMap).toBeUndefined(); + } else { + expect(model.thinkingLevelMap).toBeDefined(); + } + } + }); + + it("toggles SiliconFlow thinking with enable_thinking, including for DeepSeek V4", () => { + const models = completionsModels("siliconflow"); + expect(models.filter((model) => model.compat?.thinkingFormat === "qwen").length).toBeGreaterThan(0); + for (const model of models) { + expect(model.compat?.thinkingFormat).not.toBe("deepseek"); + } + }); + + it("uses DeepSeek-style reasoning on StepFun", () => { + for (const provider of ["stepfun", "stepfun-cn"] as const) { + expect(getBuiltinModel(provider, "step-3.7-flash").compat).toMatchObject({ + thinkingFormat: "deepseek", + requiresReasoningContentOnAssistantMessages: true, + supportsReasoningEffort: true, + }); + } + }); + + it("caps Scaleway output with max_completion_tokens", () => { + for (const model of completionsModels("scaleway")) { + expect(model.compat?.maxTokensField).toBe("max_completion_tokens"); + } + }); +}); diff --git a/packages/ai/test/image-tool-result.test.ts b/packages/ai/test/image-tool-result.test.ts index 853a02f88..7688c3879 100644 --- a/packages/ai/test/image-tool-result.test.ts +++ b/packages/ai/test/image-tool-result.test.ts @@ -417,6 +417,30 @@ describe("Tool Results with Images", () => { }); }); + describe.skipIf(!process.env.STEPFUN_API_KEY)("StepFun Provider (step-3.7-flash)", () => { + const llm = getModel("stepfun", "step-3.7-flash"); + + it("should handle tool result with only image", { retry: 3, timeout: 30000 }, async () => { + await handleToolWithImageResult(llm); + }); + + it("should handle tool result with text and image", { retry: 3, timeout: 30000 }, async () => { + await handleToolWithTextAndImageResult(llm); + }); + }); + + describe.skipIf(!process.env.STEPFUN_CN_API_KEY)("StepFun (China) Provider (step-3.7-flash)", () => { + const llm = getModel("stepfun-cn", "step-3.7-flash"); + + it("should handle tool result with only image", { retry: 3, timeout: 30000 }, async () => { + await handleToolWithImageResult(llm); + }); + + it("should handle tool result with text and image", { retry: 3, timeout: 30000 }, async () => { + await handleToolWithTextAndImageResult(llm); + }); + }); + describe.skipIf(!process.env.KIMI_API_KEY)("Kimi For Coding Provider (kimi-for-coding)", () => { const llm = getModel("kimi-coding", "kimi-for-coding"); diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 44fc907d8..df2644d4c 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -1420,6 +1420,230 @@ describe("Generate E2E Tests", () => { }); }); + describe.skipIf(!process.env.DEEPINFRA_API_KEY)("DeepInfra Provider (openai/gpt-oss-120b)", () => { + const llm = getModel("deepinfra", "openai/gpt-oss-120b"); + const thinkingOptions = { + thinkingEnabled: true, + reasoningEffort: "high", + } satisfies StreamOptionsWithExtras; + + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm); + }); + + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm); + }); + + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm); + }); + + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, thinkingOptions); + }); + + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, thinkingOptions); + }); + }); + + describe.skipIf(!process.env.NEBIUS_API_KEY)("Nebius Token Factory Provider (openai/gpt-oss-120b)", () => { + const llm = getModel("nebius", "openai/gpt-oss-120b"); + const thinkingOptions = { + thinkingEnabled: true, + reasoningEffort: "high", + } satisfies StreamOptionsWithExtras; + + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm); + }); + + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm); + }); + + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm); + }); + + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, thinkingOptions); + }); + + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, thinkingOptions); + }); + }); + + describe.skipIf(!process.env.OLLAMA_API_KEY)("Ollama Cloud Provider (gpt-oss:120b)", () => { + const llm = getModel("ollama-cloud", "gpt-oss:120b"); + const thinkingOptions = { + thinkingEnabled: true, + reasoningEffort: "high", + } satisfies StreamOptionsWithExtras; + + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm); + }); + + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm); + }); + + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm); + }); + + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, thinkingOptions); + }); + + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, thinkingOptions); + }); + }); + + describe.skipIf(!process.env.SCALEWAY_API_KEY)("Scaleway Generative APIs Provider (gpt-oss-120b)", () => { + const llm = getModel("scaleway", "gpt-oss-120b"); + const thinkingOptions = { + thinkingEnabled: true, + reasoningEffort: "high", + } satisfies StreamOptionsWithExtras; + + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm); + }); + + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm); + }); + + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm); + }); + + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, thinkingOptions); + }); + + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, thinkingOptions); + }); + }); + + describe.skipIf(!process.env.SILICONFLOW_API_KEY)("SiliconFlow Provider (zai-org/GLM-5.2)", () => { + const llm = getModel("siliconflow", "zai-org/GLM-5.2"); + const thinkingOptions = { + thinkingEnabled: true, + reasoningEffort: "high", + } satisfies StreamOptionsWithExtras; + + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm); + }); + + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm); + }); + + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm); + }); + + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, thinkingOptions); + }); + + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, thinkingOptions); + }); + }); + + describe.skipIf(!process.env.SILICONFLOW_CN_API_KEY)("SiliconFlow (China) Provider (zai-org/GLM-5.2)", () => { + const llm = getModel("siliconflow-cn", "zai-org/GLM-5.2"); + const thinkingOptions = { + thinkingEnabled: true, + reasoningEffort: "high", + } satisfies StreamOptionsWithExtras; + + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm); + }); + + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm); + }); + + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm); + }); + + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, thinkingOptions); + }); + + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, thinkingOptions); + }); + }); + + describe.skipIf(!process.env.STEPFUN_API_KEY)("StepFun Provider (step-3.7-flash)", () => { + const llm = getModel("stepfun", "step-3.7-flash"); + const thinkingOptions = { + thinkingEnabled: true, + reasoningEffort: "high", + } satisfies StreamOptionsWithExtras; + + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm); + }); + + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm); + }); + + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm); + }); + + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, thinkingOptions); + }); + + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, thinkingOptions); + }); + }); + + describe.skipIf(!process.env.STEPFUN_CN_API_KEY)("StepFun (China) Provider (step-3.7-flash)", () => { + const llm = getModel("stepfun-cn", "step-3.7-flash"); + const thinkingOptions = { + thinkingEnabled: true, + reasoningEffort: "high", + } satisfies StreamOptionsWithExtras; + + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm); + }); + + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm); + }); + + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm); + }); + + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, thinkingOptions); + }); + + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, thinkingOptions); + }); + }); + describe.skipIf(!process.env.ANT_LING_API_KEY)("Ant Ling Provider (Ling 2.6 Flash via OpenAI Completions)", () => { const llm = getModel("ant-ling", "Ling-2.6-flash"); diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index 1120c7ea7..f09a11ee2 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -348,6 +348,70 @@ describe("Token Statistics on Abort", () => { }); }); + describe.skipIf(!process.env.DEEPINFRA_API_KEY)("DeepInfra Provider", () => { + const llm = getModel("deepinfra", "openai/gpt-oss-120b"); + + it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { + await testTokensOnAbort(llm); + }); + }); + + describe.skipIf(!process.env.NEBIUS_API_KEY)("Nebius Token Factory Provider", () => { + const llm = getModel("nebius", "openai/gpt-oss-120b"); + + it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { + await testTokensOnAbort(llm); + }); + }); + + describe.skipIf(!process.env.OLLAMA_API_KEY)("Ollama Cloud Provider", () => { + const llm = getModel("ollama-cloud", "gpt-oss:120b"); + + it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { + await testTokensOnAbort(llm); + }); + }); + + describe.skipIf(!process.env.SCALEWAY_API_KEY)("Scaleway Generative APIs Provider", () => { + const llm = getModel("scaleway", "gpt-oss-120b"); + + it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { + await testTokensOnAbort(llm); + }); + }); + + describe.skipIf(!process.env.SILICONFLOW_API_KEY)("SiliconFlow Provider", () => { + const llm = getModel("siliconflow", "zai-org/GLM-5.2"); + + it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { + await testTokensOnAbort(llm); + }); + }); + + describe.skipIf(!process.env.SILICONFLOW_CN_API_KEY)("SiliconFlow (China) Provider", () => { + const llm = getModel("siliconflow-cn", "zai-org/GLM-5.2"); + + it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { + await testTokensOnAbort(llm); + }); + }); + + describe.skipIf(!process.env.STEPFUN_API_KEY)("StepFun Provider", () => { + const llm = getModel("stepfun", "step-3.7-flash"); + + it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { + await testTokensOnAbort(llm); + }); + }); + + describe.skipIf(!process.env.STEPFUN_CN_API_KEY)("StepFun (China) Provider", () => { + const llm = getModel("stepfun-cn", "step-3.7-flash"); + + it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { + await testTokensOnAbort(llm); + }); + }); + // ========================================================================= // OAuth-based providers (credentials from ~/.pi/agent/oauth.json) // ========================================================================= diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 584d6bc35..7a275d0ff 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -318,6 +318,70 @@ describe("Tool Call Without Result Tests", () => { }); }); + describe.skipIf(!process.env.DEEPINFRA_API_KEY)("DeepInfra Provider", () => { + const model = getModel("deepinfra", "openai/gpt-oss-120b"); + + it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { + await testToolCallWithoutResult(model); + }); + }); + + describe.skipIf(!process.env.NEBIUS_API_KEY)("Nebius Token Factory Provider", () => { + const model = getModel("nebius", "openai/gpt-oss-120b"); + + it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { + await testToolCallWithoutResult(model); + }); + }); + + describe.skipIf(!process.env.OLLAMA_API_KEY)("Ollama Cloud Provider", () => { + const model = getModel("ollama-cloud", "gpt-oss:120b"); + + it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { + await testToolCallWithoutResult(model); + }); + }); + + describe.skipIf(!process.env.SCALEWAY_API_KEY)("Scaleway Generative APIs Provider", () => { + const model = getModel("scaleway", "gpt-oss-120b"); + + it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { + await testToolCallWithoutResult(model); + }); + }); + + describe.skipIf(!process.env.SILICONFLOW_API_KEY)("SiliconFlow Provider", () => { + const model = getModel("siliconflow", "zai-org/GLM-5.2"); + + it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { + await testToolCallWithoutResult(model); + }); + }); + + describe.skipIf(!process.env.SILICONFLOW_CN_API_KEY)("SiliconFlow (China) Provider", () => { + const model = getModel("siliconflow-cn", "zai-org/GLM-5.2"); + + it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { + await testToolCallWithoutResult(model); + }); + }); + + describe.skipIf(!process.env.STEPFUN_API_KEY)("StepFun Provider", () => { + const model = getModel("stepfun", "step-3.7-flash"); + + it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { + await testToolCallWithoutResult(model); + }); + }); + + describe.skipIf(!process.env.STEPFUN_CN_API_KEY)("StepFun (China) Provider", () => { + const model = getModel("stepfun-cn", "step-3.7-flash"); + + it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { + await testToolCallWithoutResult(model); + }); + }); + describe.skipIf(!process.env.KIMI_API_KEY)("Kimi For Coding Provider", () => { const model = getModel("kimi-coding", "kimi-for-coding"); diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index 84cf1bfa0..0e25df0ba 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -731,6 +731,174 @@ describe("totalTokens field", () => { }); }); + describe.skipIf(!process.env.DEEPINFRA_API_KEY)("DeepInfra", () => { + it( + "openai/gpt-oss-120b - should return totalTokens equal to sum of components", + { retry: 3, timeout: 60000 }, + async () => { + const llm = getModel("deepinfra", "openai/gpt-oss-120b"); + + console.log(`\nDeepInfra / ${llm.id}:`); + const { first, second } = await testTotalTokensWithCache(llm, { + apiKey: process.env.DEEPINFRA_API_KEY, + }); + + logUsage("First request", first); + logUsage("Second request", second); + + assertTotalTokensEqualsComponents(first); + assertTotalTokensEqualsComponents(second); + }, + ); + }); + + describe.skipIf(!process.env.NEBIUS_API_KEY)("Nebius Token Factory", () => { + it( + "openai/gpt-oss-120b - should return totalTokens equal to sum of components", + { retry: 3, timeout: 60000 }, + async () => { + const llm = getModel("nebius", "openai/gpt-oss-120b"); + + console.log(`\nNebius Token Factory / ${llm.id}:`); + const { first, second } = await testTotalTokensWithCache(llm, { + apiKey: process.env.NEBIUS_API_KEY, + }); + + logUsage("First request", first); + logUsage("Second request", second); + + assertTotalTokensEqualsComponents(first); + assertTotalTokensEqualsComponents(second); + }, + ); + }); + + describe.skipIf(!process.env.OLLAMA_API_KEY)("Ollama Cloud", () => { + it( + "gpt-oss:120b - should return totalTokens equal to sum of components", + { retry: 3, timeout: 60000 }, + async () => { + const llm = getModel("ollama-cloud", "gpt-oss:120b"); + + console.log(`\nOllama Cloud / ${llm.id}:`); + const { first, second } = await testTotalTokensWithCache(llm, { + apiKey: process.env.OLLAMA_API_KEY, + }); + + logUsage("First request", first); + logUsage("Second request", second); + + assertTotalTokensEqualsComponents(first); + assertTotalTokensEqualsComponents(second); + }, + ); + }); + + describe.skipIf(!process.env.SCALEWAY_API_KEY)("Scaleway Generative APIs", () => { + it( + "gpt-oss-120b - should return totalTokens equal to sum of components", + { retry: 3, timeout: 60000 }, + async () => { + const llm = getModel("scaleway", "gpt-oss-120b"); + + console.log(`\nScaleway Generative APIs / ${llm.id}:`); + const { first, second } = await testTotalTokensWithCache(llm, { + apiKey: process.env.SCALEWAY_API_KEY, + }); + + logUsage("First request", first); + logUsage("Second request", second); + + assertTotalTokensEqualsComponents(first); + assertTotalTokensEqualsComponents(second); + }, + ); + }); + + describe.skipIf(!process.env.SILICONFLOW_API_KEY)("SiliconFlow", () => { + it( + "zai-org/GLM-5.2 - should return totalTokens equal to sum of components", + { retry: 3, timeout: 60000 }, + async () => { + const llm = getModel("siliconflow", "zai-org/GLM-5.2"); + + console.log(`\nSiliconFlow / ${llm.id}:`); + const { first, second } = await testTotalTokensWithCache(llm, { + apiKey: process.env.SILICONFLOW_API_KEY, + }); + + logUsage("First request", first); + logUsage("Second request", second); + + assertTotalTokensEqualsComponents(first); + assertTotalTokensEqualsComponents(second); + }, + ); + }); + + describe.skipIf(!process.env.SILICONFLOW_CN_API_KEY)("SiliconFlow (China)", () => { + it( + "zai-org/GLM-5.2 - should return totalTokens equal to sum of components", + { retry: 3, timeout: 60000 }, + async () => { + const llm = getModel("siliconflow-cn", "zai-org/GLM-5.2"); + + console.log(`\nSiliconFlow (China) / ${llm.id}:`); + const { first, second } = await testTotalTokensWithCache(llm, { + apiKey: process.env.SILICONFLOW_CN_API_KEY, + }); + + logUsage("First request", first); + logUsage("Second request", second); + + assertTotalTokensEqualsComponents(first); + assertTotalTokensEqualsComponents(second); + }, + ); + }); + + describe.skipIf(!process.env.STEPFUN_API_KEY)("StepFun", () => { + it( + "step-3.7-flash - should return totalTokens equal to sum of components", + { retry: 3, timeout: 60000 }, + async () => { + const llm = getModel("stepfun", "step-3.7-flash"); + + console.log(`\nStepFun / ${llm.id}:`); + const { first, second } = await testTotalTokensWithCache(llm, { + apiKey: process.env.STEPFUN_API_KEY, + }); + + logUsage("First request", first); + logUsage("Second request", second); + + assertTotalTokensEqualsComponents(first); + assertTotalTokensEqualsComponents(second); + }, + ); + }); + + describe.skipIf(!process.env.STEPFUN_CN_API_KEY)("StepFun (China)", () => { + it( + "step-3.7-flash - should return totalTokens equal to sum of components", + { retry: 3, timeout: 60000 }, + async () => { + const llm = getModel("stepfun-cn", "step-3.7-flash"); + + console.log(`\nStepFun (China) / ${llm.id}:`); + const { first, second } = await testTotalTokensWithCache(llm, { + apiKey: process.env.STEPFUN_CN_API_KEY, + }); + + logUsage("First request", first); + logUsage("Second request", second); + + assertTotalTokensEqualsComponents(first); + assertTotalTokensEqualsComponents(second); + }, + ); + }); + // ========================================================================= // Kimi For Coding // ========================================================================= diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index fc221ff51..d9f7a2424 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -838,6 +838,134 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => { }, ); + describe.skipIf(!process.env.DEEPINFRA_API_KEY)("DeepInfra Provider Unicode Handling", () => { + const llm = getModel("deepinfra", "openai/gpt-oss-120b"); + + it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { + await testEmojiInToolResults(llm); + }); + + it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { + await testRealWorldLinkedInData(llm); + }); + + it("should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { + await testUnpairedHighSurrogate(llm); + }); + }); + + describe.skipIf(!process.env.NEBIUS_API_KEY)("Nebius Token Factory Provider Unicode Handling", () => { + const llm = getModel("nebius", "openai/gpt-oss-120b"); + + it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { + await testEmojiInToolResults(llm); + }); + + it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { + await testRealWorldLinkedInData(llm); + }); + + it("should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { + await testUnpairedHighSurrogate(llm); + }); + }); + + describe.skipIf(!process.env.OLLAMA_API_KEY)("Ollama Cloud Provider Unicode Handling", () => { + const llm = getModel("ollama-cloud", "gpt-oss:120b"); + + it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { + await testEmojiInToolResults(llm); + }); + + it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { + await testRealWorldLinkedInData(llm); + }); + + it("should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { + await testUnpairedHighSurrogate(llm); + }); + }); + + describe.skipIf(!process.env.SCALEWAY_API_KEY)("Scaleway Generative APIs Provider Unicode Handling", () => { + const llm = getModel("scaleway", "gpt-oss-120b"); + + it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { + await testEmojiInToolResults(llm); + }); + + it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { + await testRealWorldLinkedInData(llm); + }); + + it("should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { + await testUnpairedHighSurrogate(llm); + }); + }); + + describe.skipIf(!process.env.SILICONFLOW_API_KEY)("SiliconFlow Provider Unicode Handling", () => { + const llm = getModel("siliconflow", "zai-org/GLM-5.2"); + + it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { + await testEmojiInToolResults(llm); + }); + + it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { + await testRealWorldLinkedInData(llm); + }); + + it("should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { + await testUnpairedHighSurrogate(llm); + }); + }); + + describe.skipIf(!process.env.SILICONFLOW_CN_API_KEY)("SiliconFlow (China) Provider Unicode Handling", () => { + const llm = getModel("siliconflow-cn", "zai-org/GLM-5.2"); + + it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { + await testEmojiInToolResults(llm); + }); + + it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { + await testRealWorldLinkedInData(llm); + }); + + it("should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { + await testUnpairedHighSurrogate(llm); + }); + }); + + describe.skipIf(!process.env.STEPFUN_API_KEY)("StepFun Provider Unicode Handling", () => { + const llm = getModel("stepfun", "step-3.7-flash"); + + it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { + await testEmojiInToolResults(llm); + }); + + it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { + await testRealWorldLinkedInData(llm); + }); + + it("should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { + await testUnpairedHighSurrogate(llm); + }); + }); + + describe.skipIf(!process.env.STEPFUN_CN_API_KEY)("StepFun (China) Provider Unicode Handling", () => { + const llm = getModel("stepfun-cn", "step-3.7-flash"); + + it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { + await testEmojiInToolResults(llm); + }); + + it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { + await testRealWorldLinkedInData(llm); + }); + + it("should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { + await testUnpairedHighSurrogate(llm); + }); + }); + describe.skipIf(!process.env.KIMI_API_KEY)("Kimi For Coding Provider Unicode Handling", () => { const llm = getModel("kimi-coding", "kimi-for-coding"); diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index 528bf50b4..4d9ac8439 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -67,9 +67,19 @@ This table covers providers with a single primary API-key variable. Providers th | Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | | Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | | Xiaomi MiMo Token Plan (Singapore) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | +| DeepInfra | `DEEPINFRA_API_KEY` | +| Nebius Token Factory | `NEBIUS_API_KEY` | +| Ollama Cloud | `OLLAMA_API_KEY` | +| Scaleway Generative APIs | `SCALEWAY_API_KEY` | +| SiliconFlow | `SILICONFLOW_API_KEY` | +| SiliconFlow (China) | `SILICONFLOW_CN_API_KEY` | +| StepFun | `STEPFUN_API_KEY` | +| StepFun (China) | `STEPFUN_CN_API_KEY` | The Z.AI pay-as-you-go APIs (`zai-api`, `zai-api-cn`) and Alibaba Cloud Model Studio (`alibaba`, `alibaba-cn`) bill usage to the account balance, while `zai`, `zai-coding-cn`, and the Qwen Token Plan providers draw on a plan. They read separate variables, so a key set for a plan never makes the pay-as-you-go models selectable. +`ollama-cloud` is Ollama's hosted API at `ollama.com`. For a local Ollama server, add it as a [compatible endpoint](models.md#configure-a-compatible-endpoint). + Anthropic also recognizes `ANTHROPIC_OAUTH_TOKEN` as an API credential and `ANTHROPIC_AUTH_TOKEN` as bearer authentication. With no key or token set, Anthropic uses workload identity federation when `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID` and `ANTHROPIC_IDENTITY_TOKEN_FILE` are set: the Anthropic SDK exchanges the identity token for a short-lived access token and refreshes it itself (re-reading the identity token file, so keep that file fresh for long sessions). `ANTHROPIC_SERVICE_ACCOUNT_ID` and `ANTHROPIC_WORKSPACE_ID` are passed through when set. diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index b8d9c1d89..f28b7e7c7 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -458,6 +458,14 @@ ${chalk.bold("Environment Variables:")} DASHSCOPE_CN_API_KEY - Alibaba Cloud Model Studio (DashScope) API key (China region) ZAI_PAYG_API_KEY - Z.AI pay-as-you-go API key (Global) ZAI_PAYG_CN_API_KEY - Z.AI pay-as-you-go API key (China, bigmodel.cn) + DEEPINFRA_API_KEY - DeepInfra API key + NEBIUS_API_KEY - Nebius Token Factory API key + OLLAMA_API_KEY - Ollama Cloud API key + SCALEWAY_API_KEY - Scaleway Generative APIs secret key + SILICONFLOW_API_KEY - SiliconFlow API key (Global) + SILICONFLOW_CN_API_KEY - SiliconFlow API key (China) + STEPFUN_API_KEY - StepFun API key (Global, stepfun.ai) + STEPFUN_CN_API_KEY - StepFun API key (China, stepfun.com) AWS_PROFILE - AWS profile for Amazon Bedrock AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index 5d73bdd48..1ce1d2bde 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -59,6 +59,14 @@ export const defaultModelPerProvider: Partial> = { "xiaomi-token-plan-cn": "mimo-v2.6-pro", "xiaomi-token-plan-ams": "mimo-v2.6-pro", "xiaomi-token-plan-sgp": "mimo-v2.6-pro", + deepinfra: "moonshotai/Kimi-K3", + nebius: "moonshotai/Kimi-K3", + "ollama-cloud": "kimi-k3", + scaleway: "glm-5.3", + siliconflow: "moonshotai/Kimi-K3", + "siliconflow-cn": "zai-org/GLM-5.2", + stepfun: "step-3.7-flash", + "stepfun-cn": "step-3.7-flash", alibaba: "qwen3.8-max", "alibaba-cn": "qwen3.8-max", "zai-api": "glm-5.3", diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index 1c543c754..3e8464a6b 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -702,13 +702,23 @@ describe("default model selection", () => { expect(defaultModelPerProvider["openai-codex"]).toBe("gpt-6.1-sol"); }); - test.each(["alibaba", "alibaba-cn", "zai-api", "zai-api-cn"] as const)( - "%s default is in its built-in catalog", - (provider) => { - const modelId = defaultModelPerProvider[provider]; - expect(getBuiltinModels(provider).some((model) => model.id === modelId)).toBe(true); - }, - ); + test.each([ + "alibaba", + "alibaba-cn", + "zai-api", + "zai-api-cn", + "deepinfra", + "nebius", + "ollama-cloud", + "scaleway", + "siliconflow", + "siliconflow-cn", + "stepfun", + "stepfun-cn", + ] as const)("%s default is in its built-in catalog", (provider) => { + const modelId = defaultModelPerProvider[provider]; + expect(getBuiltinModels(provider).some((model) => model.id === modelId)).toBe(true); + }); test("zai, minimax, cerebras, and ant-ling defaults track current models", () => { expect(defaultModelPerProvider.zai).toBe("glm-5.3");