diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 0dc86d6e0..1e65c5645 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added the ai& (`aiand`) provider registry entry with API-key paste login validated against `https://api.aiand.com/v1/models`. + ## [17.2.2] - 2026-07-31 ### Added diff --git a/packages/ai/src/registry/aiand.ts b/packages/ai/src/registry/aiand.ts new file mode 100644 index 000000000..18b0ff8ca --- /dev/null +++ b/packages/ai/src/registry/aiand.ts @@ -0,0 +1,22 @@ +import { createApiKeyLogin } from "./api-key-login"; +import type { OAuthLoginCallbacks } from "./oauth/types"; +import type { ProviderDefinition } from "./types"; + +export const loginAiand = createApiKeyLogin({ + providerLabel: "ai&", + authUrl: "https://console.aiand.com/api-keys", + instructions: "Copy your API key from the ai& console", + promptMessage: "Paste your ai& API key", + placeholder: "sk-...", + validation: { + kind: "models-endpoint", + provider: "ai&", + modelsUrl: "https://api.aiand.com/v1/models", + }, +}); + +export const aiandProvider = { + id: "aiand", + name: "ai&", + login: (cb: OAuthLoginCallbacks) => loginAiand(cb), +} as const satisfies ProviderDefinition; diff --git a/packages/ai/src/registry/registry.ts b/packages/ai/src/registry/registry.ts index aef49177d..23f50ed2b 100644 --- a/packages/ai/src/registry/registry.ts +++ b/packages/ai/src/registry/registry.ts @@ -1,4 +1,5 @@ import type { KnownProvider } from "@oh-my-pi/pi-catalog"; +import { aiandProvider } from "./aiand"; import { aimlApiProvider } from "./aimlapi"; import { alibabaCodingPlanProvider } from "./alibaba-coding-plan"; import { alibabaTokenPlanProvider } from "./alibaba-token-plan"; @@ -101,6 +102,7 @@ const ALL = [ gitLabDuoWorkflowProvider, alibabaCodingPlanProvider, alibabaTokenPlanProvider, + aiandProvider, aimlApiProvider, zhipuCodingPlanProvider, umansProvider, diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index da10a6102..90abffa9f 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added support for the ai& provider (`aiand`), an OpenAI-compatible inference API with dynamic model discovery (context windows, capabilities, reasoning efforts, and USD pricing from `/v1/models`) and API-key authentication via the `AIAND_API_KEY` environment variable. + ## [17.2.2] - 2026-07-31 ### Added diff --git a/packages/catalog/scripts/generate-models.ts b/packages/catalog/scripts/generate-models.ts index a60423732..8c6f86cc6 100644 --- a/packages/catalog/scripts/generate-models.ts +++ b/packages/catalog/scripts/generate-models.ts @@ -29,6 +29,7 @@ import { } from "../src/provider-models/descriptor-types"; import { PROVIDER_DESCRIPTORS } from "../src/provider-models/descriptors"; import { + AIAND_STATIC_MODELS, ALIBABA_TOKEN_PLAN_STATIC_MODELS, ANTHROPIC_CURATED_FALLBACK_MODELS, buildFireworksFastSeed, @@ -560,6 +561,12 @@ async function generateModels() { if (!authoritativeCatalogProviders.has("sakana")) { allModels.push(...SAKANA_FUGU_STATIC_MODELS); } + // Seed ai&'s documented catalog so the provider is usable when generation + // has no AIAND_API_KEY. A live org-scoped `/v1/models` snapshot is + // authoritative and replaces the seed. + if (!authoritativeCatalogProviders.has("aiand")) { + allModels.push(...AIAND_STATIC_MODELS); + } // Seed the GMI Cloud default model so a fresh install (and a regen without a // `GMI_API_KEY`) still resolves the descriptor's `defaultModel` synchronously // at boot. If live `/v1/models` discovery succeeds, it is authoritative. diff --git a/packages/catalog/src/models.json b/packages/catalog/src/models.json index b8a12476b..d190afda3 100644 --- a/packages/catalog/src/models.json +++ b/packages/catalog/src/models.json @@ -1,4 +1,262 @@ { + "aiand": { + "deepseek-ai/deepseek-v4-flash": { + "id": "deepseek-ai/deepseek-v4-flash", + "name": "DeepSeek V4 Flash", + "api": "openai-completions", + "provider": "aiand", + "baseUrl": "https://api.aiand.com/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0.15, + "output": 0.25, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 393216, + "thinking": { + "mode": "effort", + "efforts": [ + "high", + "max" + ] + } + }, + "deepseek-ai/deepseek-v4-pro": { + "id": "deepseek-ai/deepseek-v4-pro", + "name": "DeepSeek V4 Pro", + "api": "openai-completions", + "provider": "aiand", + "baseUrl": "https://api.aiand.com/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 1, + "output": 2.5, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 393216, + "thinking": { + "mode": "effort", + "efforts": [ + "high", + "max" + ] + } + }, + "google/gemma-4-31b-it": { + "id": "google/gemma-4-31b-it", + "name": "Gemma 4 31B", + "api": "openai-completions", + "provider": "aiand", + "baseUrl": "https://api.aiand.com/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0.2, + "output": 0.5, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 131072, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "moonshotai/kimi-k2.6": { + "id": "moonshotai/kimi-k2.6", + "name": "Kimi K2.6", + "api": "openai-completions", + "provider": "aiand", + "baseUrl": "https://api.aiand.com/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0.85, + "output": 3.5, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 262144, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "moonshotai/kimi-k2.7-code": { + "id": "moonshotai/kimi-k2.7-code", + "name": "Kimi K2.7 Code", + "api": "openai-completions", + "provider": "aiand", + "baseUrl": "https://api.aiand.com/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0.75, + "output": 3.5, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 262144, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "openai/gpt-oss-120b": { + "id": "openai/gpt-oss-120b", + "name": "GPT OSS 120B", + "api": "openai-completions", + "provider": "aiand", + "baseUrl": "https://api.aiand.com/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0.15, + "output": 0.6, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 131072, + "thinking": { + "mode": "effort", + "efforts": [ + "low", + "medium", + "high" + ] + } + }, + "qwen/qwen3.6-27b": { + "id": "qwen/qwen3.6-27b", + "name": "Qwen3.6 27B", + "api": "openai-completions", + "provider": "aiand", + "baseUrl": "https://api.aiand.com/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 65536, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high" + ] + } + }, + "zai-org/glm-5.1": { + "id": "zai-org/glm-5.1", + "name": "GLM 5.1", + "api": "openai-completions", + "provider": "aiand", + "baseUrl": "https://api.aiand.com/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 1.4, + "output": 4.4, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 202752, + "maxTokens": 131072, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "zai-org/glm-5.2": { + "id": "zai-org/glm-5.2", + "name": "GLM 5.2", + "api": "openai-completions", + "provider": "aiand", + "baseUrl": "https://api.aiand.com/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 1, + "output": 4, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 131072, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "max" + ] + } + } + }, "aimlapi": { "act_two": { "id": "act_two", @@ -8536,10 +8794,10 @@ "image" ], "cost": { - "input": 1.1, - "output": 5.5, - "cacheRead": 0.11, - "cacheWrite": 1.375 + "input": 1, + "output": 5, + "cacheRead": 0.1, + "cacheWrite": 1.25 }, "contextWindow": 200000, "maxTokens": 64000, @@ -8623,10 +8881,10 @@ "image" ], "cost": { - "input": 5.5, - "output": 27.5, - "cacheRead": 0.55, - "cacheWrite": 6.875 + "input": 5, + "output": 25, + "cacheRead": 0.5, + "cacheWrite": 6.25 }, "contextWindow": 200000, "maxTokens": 64000, @@ -8681,10 +8939,10 @@ "image" ], "cost": { - "input": 5.5, - "output": 27.5, - "cacheRead": 0.55, - "cacheWrite": 6.875 + "input": 5, + "output": 25, + "cacheRead": 0.5, + "cacheWrite": 6.25 }, "contextWindow": 1000000, "maxTokens": 128000, @@ -8711,10 +8969,10 @@ "image" ], "cost": { - "input": 5.5, - "output": 27.5, - "cacheRead": 0.55, - "cacheWrite": 6.875 + "input": 5, + "output": 25, + "cacheRead": 0.5, + "cacheWrite": 6.25 }, "contextWindow": 1000000, "maxTokens": 128000, @@ -8741,10 +8999,10 @@ "image" ], "cost": { - "input": 5.5, - "output": 27.5, - "cacheRead": 0.55, - "cacheWrite": 6.875 + "input": 5, + "output": 25, + "cacheRead": 0.5, + "cacheWrite": 6.25 }, "contextWindow": 1000000, "maxTokens": 128000, @@ -8937,7 +9195,7 @@ }, "global.anthropic.claude-haiku-4-5-20251001-v1:0": { "id": "global.anthropic.claude-haiku-4-5-20251001-v1:0", - "name": "Claude Haiku 4.5 (Global)", + "name": "Claude Haiku 4.5", "api": "bedrock-converse-stream", "provider": "amazon-bedrock", "baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com", @@ -10889,7 +11147,7 @@ }, "us.deepseek.r1-v1:0": { "id": "us.deepseek.r1-v1:0", - "name": "DeepSeek-R1 (US)", + "name": "DeepSeek-R1", "api": "bedrock-converse-stream", "provider": "amazon-bedrock", "baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com", @@ -11014,7 +11272,7 @@ }, "us.meta.llama4-maverick-17b-instruct-v1:0": { "id": "us.meta.llama4-maverick-17b-instruct-v1:0", - "name": "Llama 4 Maverick 17B Instruct (US)", + "name": "Llama 4 Maverick 17B Instruct", "api": "bedrock-converse-stream", "provider": "amazon-bedrock", "baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com", @@ -11034,7 +11292,7 @@ }, "us.meta.llama4-scout-17b-instruct-v1:0": { "id": "us.meta.llama4-scout-17b-instruct-v1:0", - "name": "Llama 4 Scout 17B Instruct (US)", + "name": "Llama 4 Scout 17B Instruct", "api": "bedrock-converse-stream", "provider": "amazon-bedrock", "baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com", @@ -13063,6 +13321,26 @@ } }, "baseten": { + "deepseek-ai/DeepSeek-V4-Flash-0731": { + "id": "deepseek-ai/DeepSeek-V4-Flash-0731", + "name": "Deepseek V4 Flash 0731", + "api": "openai-completions", + "provider": "baseten", + "baseUrl": "https://inference.baseten.co/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 1048576, + "supportsComputerUse": false + }, "deepseek-ai/DeepSeek-V4-Pro": { "id": "deepseek-ai/DeepSeek-V4-Pro", "name": "Deepseek V4 Pro", @@ -13312,9 +13590,9 @@ "image" ], "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, + "input": 0.5, + "output": 1.2, + "cacheRead": 0.09999999999999999, "cacheWrite": 0 }, "contextWindow": 1048576, @@ -15788,7 +16066,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "thinking": { "mode": "effort", @@ -15827,7 +16105,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -15865,7 +16143,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "thinking": { "mode": "effort", @@ -15904,7 +16182,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -15943,7 +16221,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -15982,7 +16260,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16021,7 +16299,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16060,7 +16338,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16099,7 +16377,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16138,7 +16416,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -16177,7 +16455,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16216,7 +16494,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -16255,7 +16533,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16294,7 +16572,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -16333,7 +16611,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16372,7 +16650,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -16411,7 +16689,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16450,7 +16728,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -16489,7 +16767,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16528,7 +16806,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -16567,7 +16845,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16606,7 +16884,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -16645,7 +16923,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16684,7 +16962,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -16723,7 +17001,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16762,7 +17040,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -16801,7 +17079,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16840,7 +17118,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -16879,7 +17157,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16918,7 +17196,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -16957,7 +17235,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -16996,7 +17274,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -17035,7 +17313,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -17074,7 +17352,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "thinking": { @@ -17113,7 +17391,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -17136,7 +17414,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "supportsComputerUse": false, @@ -17159,7 +17437,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -17182,7 +17460,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": true, "supportsComputerUse": false, @@ -17205,7 +17483,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -17244,7 +17522,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -17283,7 +17561,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -17322,7 +17600,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -17361,7 +17639,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "thinking": { @@ -17859,7 +18137,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -17881,7 +18159,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -18691,7 +18969,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "supportsComputerUse": false, "cursorMaxMode": false, @@ -18735,7 +19013,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "supportsComputerUse": false, "cursorMaxMode": false, @@ -18757,7 +19035,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "supportsComputerUse": false, "cursorMaxMode": false, @@ -19031,7 +19309,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "supportsComputerUse": false, "cursorMaxMode": false, @@ -19076,7 +19354,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19124,7 +19402,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19172,7 +19450,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19220,7 +19498,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19268,7 +19546,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19316,7 +19594,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19362,7 +19640,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19408,7 +19686,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19454,7 +19732,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19500,7 +19778,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19546,7 +19824,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19592,7 +19870,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19638,7 +19916,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19684,7 +19962,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19730,7 +20008,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19776,7 +20054,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19822,7 +20100,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19868,7 +20146,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19914,7 +20192,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -19960,7 +20238,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -20006,7 +20284,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -20052,7 +20330,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -20098,7 +20376,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -20236,7 +20514,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -20268,7 +20546,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -20300,7 +20578,7 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 200000, + "contextWindow": 1000000, "maxTokens": 64000, "cursorMaxMode": false, "supportsComputerUse": false, @@ -20487,6 +20765,33 @@ "requiresAssistantContentForToolCalls": true } }, + "deepseek-v4-flash-0731": { + "id": "deepseek-v4-flash-0731", + "name": "DeepSeek-V4-Flash-0731", + "api": "openai-completions", + "provider": "fireworks", + "baseUrl": "https://api.fireworks.ai/inference/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 1048576, + "thinking": { + "mode": "effort", + "efforts": [ + "high", + "max" + ] + }, + "supportsComputerUse": false + }, "deepseek-v4-pro": { "id": "deepseek-v4-pro", "name": "deepseek-v4-pro", @@ -61608,6 +61913,34 @@ "supportsComputerUse": false, "supportsComputerUseConfig": false }, + "deepseek/deepseek-v4-flash-0731": { + "id": "deepseek/deepseek-v4-flash-0731", + "name": "Deepseek V4 Flash 0731", + "api": "openai-completions", + "provider": "novita", + "baseUrl": "https://api.novita.ai/openai/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0.14, + "output": 0.28, + "cacheRead": 0.028, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 393216, + "supportsTools": true, + "thinking": { + "mode": "effort", + "efforts": [ + "high", + "max" + ] + }, + "supportsComputerUse": false + }, "deepseek/deepseek-v4-pro": { "id": "deepseek/deepseek-v4-pro", "name": "DeepSeek V4 Pro", @@ -67794,6 +68127,37 @@ ] } }, + "deepseek-v4-flash:0731": { + "id": "deepseek-v4-flash:0731", + "name": "deepseek-v4-flash:0731", + "api": "ollama-chat", + "provider": "ollama-cloud", + "baseUrl": "https://ollama.com", + "reasoning": true, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 8192, + "omitMaxOutputTokens": true, + "supportsComputerUse": false + }, "deepseek-v4-pro": { "id": "deepseek-v4-pro", "name": "deepseek-v4-pro", @@ -78744,9 +79108,9 @@ "image" ], "cost": { - "input": 0.589, - "output": 2.48, - "cacheRead": 0.0992, + "input": 0.6, + "output": 3.41, + "cacheRead": 0.19999999999999998, "cacheWrite": 0 }, "contextWindow": 262144, @@ -82543,12 +82907,12 @@ ], "cost": { "input": 0.07, - "output": 0.27, + "output": 0.28, "cacheRead": 0, "cacheWrite": 0 }, "contextWindow": 262144, - "maxTokens": 32768, + "maxTokens": 262144, "supportsComputerUse": false, "supportsComputerUseConfig": false }, @@ -83982,6 +84346,36 @@ "supportsComputerUse": false, "supportsComputerUseConfig": false }, + "thinkingmachines/inkling-small": { + "id": "thinkingmachines/inkling-small", + "name": "Inkling Small", + "api": "openrouter", + "provider": "openrouter", + "baseUrl": "https://openrouter.ai/api/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0.5, + "output": 1.2, + "cacheRead": 0.09999999999999999, + "cacheWrite": 0 + }, + "contextWindow": 524288, + "maxTokens": 32768, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high" + ] + }, + "supportsComputerUse": false + }, "tngtech/deepseek-r1t2-chimera": { "id": "tngtech/deepseek-r1t2-chimera", "name": "DeepSeek R1T2 Chimera", @@ -85011,9 +85405,9 @@ "text" ], "cost": { - "input": 1.12, - "output": 3.52, - "cacheRead": 0.20800000000000002, + "input": 0.76006, + "output": 2.38876, + "cacheRead": 0.141154, "cacheWrite": 0 }, "contextWindow": 1048576, @@ -86431,6 +86825,39 @@ "escapeBuiltinToolNames": true } }, + "umans-deepseek-v4-flash-0731": { + "id": "umans-deepseek-v4-flash-0731", + "name": "Umans DeepSeek V4 Flash (experimental)", + "api": "anthropic-messages", + "provider": "umans", + "baseUrl": "https://api.code.umans.ai", + "reasoning": true, + "thinking": { + "mode": "budget", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 393215, + "supportsComputerUse": false, + "compat": { + "escapeBuiltinToolNames": true + } + }, "umans-flash": { "id": "umans-flash", "name": "Umans Flash", @@ -87259,6 +87686,29 @@ ] } }, + "deepseek-v4-flash-0731": { + "id": "deepseek-v4-flash-0731", + "name": "deepseek-v4-flash-0731", + "api": "openai-completions", + "provider": "venice", + "baseUrl": "https://api.venice.ai/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 1048576, + "supportsComputerUse": false, + "compat": { + "supportsUsageInStreaming": false + } + }, "deepseek-v4-pro": { "id": "deepseek-v4-pro", "name": "DeepSeek V4 Pro", @@ -91973,8 +92423,38 @@ "text" ], "cost": { - "input": 0.13799999999999998, - "output": 0.275, + "input": 0.14, + "output": 0.28, + "cacheRead": 0.0028, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 384000, + "thinking": { + "mode": "budget", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "supportsComputerUse": false + }, + "deepseek/deepseek-v4-flash-0731": { + "id": "deepseek/deepseek-v4-flash-0731", + "name": "DeepSeek V4 Flash 0731", + "api": "anthropic-messages", + "provider": "vercel-ai-gateway", + "baseUrl": "https://ai-gateway.vercel.sh", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0.13, + "output": 0.26, "cacheRead": 0.028, "cacheWrite": 0 }, @@ -105158,6 +105638,37 @@ ] } }, + "glm-5.2-highspeed[1m]": { + "id": "glm-5.2-highspeed[1m]", + "name": "GLM-5.2 Highspeed", + "api": "openai-completions", + "provider": "zhipu-coding-plan", + "baseUrl": "https://open.bigmodel.cn/api/coding/paas/v4", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 131072, + "compat": { + "thinkingFormat": "zai", + "reasoningContentField": "reasoning_content", + "supportsDeveloperRole": false + }, + "thinking": { + "mode": "effort", + "efforts": [ + "high", + "max" + ] + } + }, "glm-5v-turbo": { "id": "glm-5v-turbo", "name": "GLM-5V-Turbo", diff --git a/packages/catalog/src/provider-models/descriptors.ts b/packages/catalog/src/provider-models/descriptors.ts index 132eff52e..82b2d5239 100644 --- a/packages/catalog/src/provider-models/descriptors.ts +++ b/packages/catalog/src/provider-models/descriptors.ts @@ -9,6 +9,7 @@ import type { ModelManagerConfig, ProviderCatalogEntry, ProviderDescriptor } fro import { googleModelManagerOptions, googleVertexModelManagerOptions } from "./google"; import { ollamaCloudModelManagerOptions } from "./ollama"; import { + aiandModelManagerOptions, aimlApiModelManagerOptions, alibabaCodingPlanModelManagerOptions, alibabaTokenPlanModelManagerOptions, @@ -65,6 +66,14 @@ import { } from "./special"; export const CATALOG_PROVIDERS = [ + { + id: "aiand", + defaultModel: "moonshotai/kimi-k2.7-code", + envVars: ["AIAND_API_KEY"], + createModelManagerOptions: (config: ModelManagerConfig) => aiandModelManagerOptions(config), + dynamicModelsAuthoritative: true, + catalogDiscovery: { label: "ai&" }, + }, { id: "aimlapi", defaultModel: "gpt-5.5-2026-04-23", diff --git a/packages/catalog/src/provider-models/openai-compat.ts b/packages/catalog/src/provider-models/openai-compat.ts index 04c791c02..837cd1991 100644 --- a/packages/catalog/src/provider-models/openai-compat.ts +++ b/packages/catalog/src/provider-models/openai-compat.ts @@ -3796,6 +3796,175 @@ export function sakanaModelManagerOptions(config?: SakanaModelManagerConfig): Mo }; } +// --------------------------------------------------------------------------- +// 16.6 ai& (aiand.com) +// --------------------------------------------------------------------------- + +const AIAND_DEFAULT_BASE_URL = "https://api.aiand.com/v1"; + +/** `reasoning_efforts` wire values ai& reports, mapped onto pi effort levels. */ +const AIAND_EFFORT_BY_WIRE_VALUE: Record = { + minimal: Effort.Minimal, + low: Effort.Low, + medium: Effort.Medium, + high: Effort.High, + xhigh: Effort.XHigh, + max: Effort.Max, +}; + +function normalizeAiandBaseUrl(baseUrl: string | undefined): string { + const value = baseUrl?.trim() || AIAND_DEFAULT_BASE_URL; + const normalized = value.replace(/\/+$/, ""); + return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`; +} + +function createAiandStaticModel( + id: string, + name: string, + cost: { input: number; output: number }, + contextWindow: number, + input: ModelSpec<"openai-completions">["input"], +): ModelSpec<"openai-completions"> { + return { + id, + name, + api: "openai-completions", + provider: "aiand", + baseUrl: AIAND_DEFAULT_BASE_URL, + reasoning: true, + input: [...input], + cost: { input: cost.input, output: cost.output, cacheRead: 0, cacheWrite: 0 }, + contextWindow, + maxTokens: null, + thinking: { mode: "effort", efforts: [Effort.Low, Effort.Medium, Effort.High], defaultLevel: Effort.Medium }, + }; +} + +/** + * Documented ai& catalog (docs.aiand.com/models/catalog, 2026-08) bundled so + * the provider is usable when generation and first boot have no live key. + * The org-scoped `/v1/models` response is authoritative once discovery runs. + */ +export const AIAND_STATIC_MODELS: readonly ModelSpec<"openai-completions">[] = [ + createAiandStaticModel("qwen/qwen3.6-27b", "Qwen3.6 27B", { input: 0, output: 0 }, 262_144, ["text"]), + createAiandStaticModel( + "deepseek-ai/deepseek-v4-flash", + "DeepSeek V4 Flash", + { input: 0.15, output: 0.25 }, + 1_000_000, + ["text"], + ), + createAiandStaticModel("google/gemma-4-31b-it", "Gemma 4 31B IT", { input: 0.2, output: 0.5 }, 262_144, [ + "text", + "image", + ]), + createAiandStaticModel("openai/gpt-oss-120b", "GPT OSS 120B", { input: 0.15, output: 0.6 }, 131_072, ["text"]), + createAiandStaticModel("deepseek-ai/deepseek-v4-pro", "DeepSeek V4 Pro", { input: 1, output: 2.5 }, 1_000_000, [ + "text", + ]), + createAiandStaticModel("moonshotai/kimi-k2.7-code", "Kimi K2.7 Code", { input: 0.75, output: 3.5 }, 262_144, [ + "text", + "image", + ]), + createAiandStaticModel("moonshotai/kimi-k2.6", "Kimi K2.6", { input: 0.85, output: 3.5 }, 262_144, [ + "text", + "image", + ]), + createAiandStaticModel("zai-org/glm-5.2", "GLM 5.2", { input: 1, output: 4 }, 1_000_000, ["text"]), + createAiandStaticModel("zai-org/glm-5.1", "GLM 5.1", { input: 1.4, output: 4.4 }, 202_752, ["text"]), +]; + +const AIAND_STATIC_MODEL_IDS = AIAND_STATIC_MODELS.map(model => model.id); + +function mapAiandThinking(entry: OpenAICompatibleModelRecord): ThinkingConfig | undefined { + const efforts = Array.isArray(entry.reasoning_efforts) + ? entry.reasoning_efforts.flatMap(value => + typeof value === "string" && AIAND_EFFORT_BY_WIRE_VALUE[value] ? [AIAND_EFFORT_BY_WIRE_VALUE[value]] : [], + ) + : []; + if (efforts.length === 0) { + return undefined; + } + const defaultLevel = + typeof entry.reasoning_effort_default === "string" + ? AIAND_EFFORT_BY_WIRE_VALUE[entry.reasoning_effort_default] + : undefined; + return { + mode: "effort", + efforts, + ...(defaultLevel && efforts.includes(defaultLevel) && { defaultLevel }), + }; +} + +/** + * ai& reports prices as decimal strings per 1M tokens in the org's billing + * currency (`usd` or `jpy`). Costs are only mapped for USD orgs — JPY figures + * would corrupt the USD-denominated cost model, so they fall back to zero. + */ +function mapAiandCost(entry: OpenAICompatibleModelRecord): ModelSpec<"openai-completions">["cost"] { + if (typeof entry.currency === "string" && entry.currency !== "usd") { + return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; + } + return { + input: toPositiveNumber(entry.input_per_1m, 0), + output: toPositiveNumber(entry.output_per_1m, 0), + cacheRead: 0, + cacheWrite: 0, + }; +} + +function mapAiandModel( + entry: OpenAICompatibleModelRecord, + defaults: ModelSpec<"openai-completions">, +): ModelSpec<"openai-completions"> { + const capabilities: unknown[] = Array.isArray(entry.capabilities) ? entry.capabilities : []; + const reasoning = capabilities.includes("reasoning"); + const thinking = reasoning ? mapAiandThinking(entry) : undefined; + const description = + typeof entry.description === "string" && entry.description.trim() ? entry.description : undefined; + return { + ...defaults, + name: description ?? toModelName(entry.name, defaults.name), + reasoning, + input: capabilities.includes("vision") ? ["text", "image"] : ["text"], + cost: mapAiandCost(entry), + contextWindow: toPositiveNumber(entry.context_window, null), + ...(thinking && { thinking }), + }; +} + +export interface AiandModelManagerConfig { + apiKey?: string; + baseUrl?: string; + fetch?: FetchImpl; +} + +/** + * ai& (aiand.com) model manager: OpenAI-compatible chat completions with an + * org-scoped `/v1/models` catalog carrying context, capability, effort, and + * pricing metadata, so discovery is authoritative over the bundled seed. + */ +export function aiandModelManagerOptions(config?: AiandModelManagerConfig): ModelManagerOptions<"openai-completions"> { + const apiKey = config?.apiKey; + const baseUrl = normalizeAiandBaseUrl(config?.baseUrl ?? Bun.env.AIAND_BASE_URL); + return { + providerId: "aiand", + dynamicModelsAuthoritative: true, + dropCachedModelIdsOnStaticMismatch: AIAND_STATIC_MODEL_IDS, + ...(apiKey && { + fetchDynamicModels: () => + fetchOpenAICompatibleModels({ + api: "openai-completions", + provider: "aiand", + baseUrl, + apiKey, + mapModel: (entry, defaults) => mapAiandModel(entry, defaults), + fetch: config?.fetch, + }), + }), + }; +} + // --------------------------------------------------------------------------- // 17. Qwen Portal // --------------------------------------------------------------------------- diff --git a/packages/catalog/test/aiand-provider.test.ts b/packages/catalog/test/aiand-provider.test.ts new file mode 100644 index 000000000..6bbc97f69 --- /dev/null +++ b/packages/catalog/test/aiand-provider.test.ts @@ -0,0 +1,156 @@ +import { afterEach, describe, expect, test, vi } from "bun:test"; +import { getOAuthProviders } from "@oh-my-pi/pi-ai/registry/oauth"; +import { getEnvApiKey } from "@oh-my-pi/pi-ai/stream"; +import { Effort } from "@oh-my-pi/pi-catalog/effort"; +import { getBundledModels } from "@oh-my-pi/pi-catalog/models"; +import { DEFAULT_MODEL_PER_PROVIDER, PROVIDER_DESCRIPTORS } from "@oh-my-pi/pi-catalog/provider-models/descriptors"; +import { aiandModelManagerOptions } from "@oh-my-pi/pi-catalog/provider-models/openai-compat"; +import type { FetchImpl } from "@oh-my-pi/pi-catalog/types"; + +const ORIGINAL_ENV = { + AIAND_API_KEY: Bun.env.AIAND_API_KEY, + AIAND_BASE_URL: Bun.env.AIAND_BASE_URL, +} as const; + +function restoreEnvVar(name: keyof typeof ORIGINAL_ENV): void { + const value = ORIGINAL_ENV[name]; + if (value === undefined) { + delete Bun.env[name]; + return; + } + Bun.env[name] = value; +} + +afterEach(() => { + restoreEnvVar("AIAND_API_KEY"); + restoreEnvVar("AIAND_BASE_URL"); + vi.restoreAllMocks(); +}); + +/** One entry in ai&'s documented `/v1/models` OpenAI-surface response shape. */ +function aiandModelsResponse(entries: Record[]): Response { + return new Response(JSON.stringify({ object: "list", data: entries }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +describe("ai& provider support", () => { + test("resolves the AIAND_API_KEY environment fallback", () => { + Bun.env.AIAND_API_KEY = "aiand-test-key"; + expect(getEnvApiKey("aiand")).toBe("aiand-test-key"); + }); + + test("registers descriptor, default model, bundled seed, and login provider", () => { + const descriptor = PROVIDER_DESCRIPTORS.find(item => item.providerId === "aiand"); + expect(descriptor).toBeDefined(); + expect(descriptor?.defaultModel).toBe("moonshotai/kimi-k2.7-code"); + expect(descriptor?.dynamicModelsAuthoritative).toBe(true); + expect(DEFAULT_MODEL_PER_PROVIDER.aiand).toBe("moonshotai/kimi-k2.7-code"); + + const bundled = getBundledModels("aiand"); + const defaultModel = bundled.find(model => model.id === "moonshotai/kimi-k2.7-code"); + expect(defaultModel).toBeDefined(); + for (const model of bundled) { + expect(model.api).toBe("openai-completions"); + expect(model.baseUrl).toBe("https://api.aiand.com/v1"); + } + + const provider = getOAuthProviders().find(item => item.id === "aiand"); + expect(provider?.name).toBe("ai&"); + }); + + test("maps ai& /v1/models metadata: context, capabilities, efforts, and USD pricing", async () => { + delete Bun.env.AIAND_BASE_URL; + const fetchMock: FetchImpl = vi.fn(async () => + aiandModelsResponse([ + { + id: "openai/gpt-oss-120b", + name: "openai/gpt-oss-120b", + description: "OpenAI GPT OSS 120B", + context_window: 131072, + capabilities: ["reasoning", "tool_calling"], + reasoning_efforts: ["low", "medium", "high"], + reasoning_effort_default: "medium", + currency: "usd", + input_per_1m: "0.150000", + output_per_1m: "0.600000", + }, + { + id: "google/gemma-4-31b-it", + name: "google/gemma-4-31b-it", + context_window: 262144, + capabilities: ["tool_calling", "vision", "video", "document"], + reasoning_efforts: null, + currency: "usd", + input_per_1m: "0.200000", + output_per_1m: "0.500000", + }, + ]), + ) as unknown as FetchImpl; + + const options = aiandModelManagerOptions({ apiKey: "aiand-key", fetch: fetchMock }); + expect(options.dynamicModelsAuthoritative).toBe(true); + const models = await options.fetchDynamicModels?.(); + + expect(fetchMock).toHaveBeenCalledWith( + "https://api.aiand.com/v1/models", + expect.objectContaining({ + method: "GET", + headers: expect.objectContaining({ Authorization: "Bearer aiand-key" }), + }), + ); + + const gptOss = models?.find(model => model.id === "openai/gpt-oss-120b"); + expect(gptOss?.name).toBe("OpenAI GPT OSS 120B"); + expect(gptOss?.reasoning).toBe(true); + expect(gptOss?.thinking?.efforts).toEqual([Effort.Low, Effort.Medium, Effort.High]); + expect(gptOss?.thinking?.defaultLevel).toBe(Effort.Medium); + expect(gptOss?.contextWindow).toBe(131072); + expect(gptOss?.cost).toEqual({ input: 0.15, output: 0.6, cacheRead: 0, cacheWrite: 0 }); + expect(gptOss?.input).toEqual(["text"]); + + const gemma = models?.find(model => model.id === "google/gemma-4-31b-it"); + expect(gemma?.reasoning).toBe(false); + expect(gemma?.thinking).toBeUndefined(); + expect(gemma?.input).toEqual(["text", "image"]); + }); + + test("ignores non-USD pricing so JPY orgs do not corrupt USD cost accounting", async () => { + const fetchMock: FetchImpl = vi.fn(async () => + aiandModelsResponse([ + { + id: "zai-org/glm-5.2", + context_window: 1000000, + capabilities: ["reasoning", "tool_calling"], + currency: "jpy", + input_per_1m: "150.000000", + output_per_1m: "600.000000", + }, + ]), + ) as unknown as FetchImpl; + + const options = aiandModelManagerOptions({ apiKey: "aiand-key", fetch: fetchMock }); + const models = await options.fetchDynamicModels?.(); + expect(models?.[0]?.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }); + }); + + test("prefers explicit base URL over AIAND_BASE_URL and appends /v1", async () => { + Bun.env.AIAND_BASE_URL = "https://env.aiand.test"; + const fetchMock: FetchImpl = vi.fn(async () => + aiandModelsResponse([{ id: "openai/gpt-oss-120b" }]), + ) as unknown as FetchImpl; + + const options = aiandModelManagerOptions({ + apiKey: "aiand-key", + baseUrl: "https://config.aiand.test/", + fetch: fetchMock, + }); + await options.fetchDynamicModels?.(); + + expect(fetchMock).toHaveBeenCalledWith( + "https://config.aiand.test/v1/models", + expect.objectContaining({ method: "GET" }), + ); + }); +}); diff --git a/packages/catalog/test/cursor-discovery.test.ts b/packages/catalog/test/cursor-discovery.test.ts index 7a278a01a..eb758911c 100644 --- a/packages/catalog/test/cursor-discovery.test.ts +++ b/packages/catalog/test/cursor-discovery.test.ts @@ -288,10 +288,13 @@ describe("fetchCursorUsableModels", () => { }); it("keeps the default window for unlabeled non-max models and max-mode models outside 1M families", async () => { + // Unbundled ids: the contract under test is "no 1M signal → fallback + // preserved", so neither id may carry a bundled cursor reference whose + // snapshot window would replace the 200k default fallback. const response = create(GetUsableModelsResponseSchema, { models: [ create(ModelDetailsSchema, { modelId: "cursor-composer-max", maxMode: true }), - create(ModelDetailsSchema, { modelId: "claude-opus-4-8-high", displayName: "Opus 4.8" }), + create(ModelDetailsSchema, { modelId: "claude-opus-9-high", displayName: "Opus 9" }), ], }); const defaultBaseUrl = await startCursorDiscoveryServer(toBinary(GetUsableModelsResponseSchema, response)); @@ -299,7 +302,7 @@ describe("fetchCursorUsableModels", () => { const models = await fetchCursorUsableModels({ apiKey: "test-token", baseUrl: defaultBaseUrl, timeoutMs: 1_000 }); expect(models).toEqual([ - expect.objectContaining({ id: "claude-opus-4-8-high", cursorMaxMode: false, contextWindow: 200_000 }), + expect.objectContaining({ id: "claude-opus-9-high", cursorMaxMode: false, contextWindow: 200_000 }), expect.objectContaining({ id: "cursor-composer-max", cursorMaxMode: true, contextWindow: 200_000 }), ]); }); diff --git a/packages/coding-agent/test/task/task-batch.test.ts b/packages/coding-agent/test/task/task-batch.test.ts index c51535c00..c2372bb35 100644 --- a/packages/coding-agent/test/task/task-batch.test.ts +++ b/packages/coding-agent/test/task/task-batch.test.ts @@ -154,7 +154,7 @@ describe("task.batch schema gating", () => { expect(batch.description).toContain("`effort`"); }); - it("keeps isolation boolean-only and describes the configured apply behavior", async () => { + it("keeps isolation boolean-only in the batch item schema", async () => { mockDiscovery(); const tool = await TaskTool.create( @@ -169,18 +169,6 @@ describe("task.batch schema gating", () => { } expect(isolatedSchema.type).toBe("boolean"); expect(itemProperties.apply).toBeUndefined(); - expect(tool.description).toContain("automatically applied to the parent checkout"); - - const captureTool = await TaskTool.create( - createSession({ - settings: { - "task.batch": true, - "task.isolation.mode": "auto", - "task.isolation.apply": false, - }, - }), - ); - expect(captureTool.description).toContain("without modifying the parent checkout"); }); it("hides isolation from the dynamic batch schema in plan mode", async () => {