mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
This builds the model catalog from per-provider definitions and models.dev v2 data. It eplacer `generate-models.ts` with one `defineProvider()` file per built-in provider, shared model-family rules (Claude, GPT, DeepSeek, Kimi, Google), and source adapters. models.dev is read through its v2 API. Outputs and CLI flags are unchanged.
206 lines
8.2 KiB
TypeScript
206 lines
8.2 KiB
TypeScript
import type { Api, ClassifierModel, Model, OpenAICompletionsCompat, OpenAIResponsesCompat } from "../../src/types.ts";
|
|
import {
|
|
type ChatModel,
|
|
defineProvider,
|
|
mergeCompat,
|
|
mergeThinking,
|
|
modelsDevModels,
|
|
type ProviderContext,
|
|
} from "../define.ts";
|
|
import { supportsClaudeMidConvoSystemMessages } from "../families/anthropic.ts";
|
|
import { googleThinkingLevels } from "../families/google.ts";
|
|
import { isKimiK3FamilyId } from "../families/kimi.ts";
|
|
import { enableOpenAiGrammarTools, enableOpenAiMidConvoSystemMessages } from "../families/openai.ts";
|
|
import { getEffortThinkingLevelMap } from "../reasoning.ts";
|
|
import type { ModelsDevModel } from "../sources/models-dev.ts";
|
|
|
|
// OpenAI-compatible endpoints reject the long prompt-cache retention parameter for these models.
|
|
const LONG_CACHE_RETENTION_UNSUPPORTED_MODELS = new Set([
|
|
"opencode:deepseek-v4-flash",
|
|
"opencode:deepseek-v4.1-flash",
|
|
"opencode:deepseek-v4-pro",
|
|
"opencode:kimi-k2.5",
|
|
"opencode:kimi-k2.6",
|
|
"opencode:minimax-m2.7",
|
|
"opencode-go:kimi-k2.6",
|
|
]);
|
|
|
|
// OpenCode serves each model on one protocol-specific endpoint. Native protocols
|
|
// come before generic Chat Completions if models.dev ever lists several.
|
|
const OPENCODE_PROTOCOLS = ["responses", "messages", "generate-content", "chat-completions"] as const;
|
|
|
|
interface OpencodeApi {
|
|
api: Api;
|
|
baseUrl: string;
|
|
compat?: OpenAICompletionsCompat | OpenAIResponsesCompat;
|
|
}
|
|
|
|
/**
|
|
* The API and base URL models.dev lists for a model. Models served only on a
|
|
* protocol pi does not support fail strict generation and are skipped otherwise.
|
|
*/
|
|
function opencodeApi(ctx: ProviderContext, source: ModelsDevModel): OpencodeApi | undefined {
|
|
const entry = OPENCODE_PROTOCOLS.map((protocol) =>
|
|
source.apis.find((candidate) => candidate.protocol === protocol),
|
|
).find((candidate) => candidate !== undefined);
|
|
if (!entry) {
|
|
const protocols = source.apis.map((candidate) => candidate.protocol).join(", ") || "none";
|
|
const message = `models.dev lists no supported API for ${ctx.providerId}/${source.id} (${protocols})`;
|
|
if (ctx.strict) throw new Error(message);
|
|
console.warn(`Skipping model: ${message}`);
|
|
return undefined;
|
|
}
|
|
switch (entry.protocol) {
|
|
case "responses":
|
|
return {
|
|
api: "openai-responses",
|
|
baseUrl: entry.baseUrl,
|
|
compat: { sessionAffinityFormat: "openai-nosession" },
|
|
};
|
|
case "messages":
|
|
// The Anthropic SDK appends /v1/messages itself.
|
|
return { api: "anthropic-messages", baseUrl: entry.baseUrl.replace(/\/v1\/?$/, "") };
|
|
case "generate-content":
|
|
return { api: "google-generative-ai", baseUrl: entry.baseUrl };
|
|
case "chat-completions":
|
|
return {
|
|
api: "openai-completions",
|
|
baseUrl: entry.baseUrl,
|
|
...(entry.aisdk === "@ai-sdk/alibaba" ? { compat: { cacheControlFormat: "anthropic" } } : {}),
|
|
};
|
|
}
|
|
}
|
|
|
|
/** OpenCode Zen and Go models, on the endpoints models.dev lists for them. */
|
|
export function opencodeModels(ctx: ProviderContext): ChatModel[] {
|
|
const provider = ctx.providerId;
|
|
return modelsDevModels(ctx, provider, {
|
|
api: "openai-completions",
|
|
// Replaced by the base URL of the API models.dev lists for each model.
|
|
baseUrl: "",
|
|
skipDeprecated: true,
|
|
// Codex Spark is not usable through OpenCode.
|
|
include: (source) => source.id !== "gpt-5.3-codex-spark",
|
|
map: (model, source) => {
|
|
const served = opencodeApi(ctx, source);
|
|
if (!served) return undefined;
|
|
let { api, baseUrl, compat } = served;
|
|
if (provider === "opencode" && model.id === "grok-build-0.1") {
|
|
compat = { ...compat, supportsReasoningEffort: false };
|
|
}
|
|
if (model.id === "kimi-k2.6") {
|
|
// OpenCode Kimi K2.6 accepts Anthropic-style thinking objects
|
|
// and rejects string thinking values or combined reasoning_effort.
|
|
compat = { ...compat, thinkingFormat: "deepseek", supportsReasoningEffort: false };
|
|
}
|
|
// models.dev reports these as @ai-sdk/anthropic, but OpenCode Go serves
|
|
// them through the OpenAI-compatible /openai/v1/chat/completions path.
|
|
if (
|
|
provider === "opencode-go" &&
|
|
api === "anthropic-messages" &&
|
|
(model.id === "qwen3.5-plus" || model.id === "qwen3.6-plus")
|
|
) {
|
|
api = "openai-completions";
|
|
baseUrl = baseUrl.replace(/\/anthropic$/, "/openai/v1");
|
|
// Qwen/DashScope uses enable_thinking at the top level.
|
|
compat = { ...compat, thinkingFormat: "qwen" };
|
|
}
|
|
if (api === "openai-completions") {
|
|
compat = {
|
|
...compat,
|
|
maxTokensField: "max_tokens",
|
|
...(LONG_CACHE_RETENTION_UNSUPPORTED_MODELS.has(`${provider}:${model.id}`)
|
|
? { supportsLongCacheRetention: false }
|
|
: {}),
|
|
};
|
|
}
|
|
const thinkingLevelMap =
|
|
api === "google-generative-ai"
|
|
? googleThinkingLevels(model.id, source.reasoningOptions)
|
|
: provider === "opencode-go" && model.id === "deepseek-v4.1-flash"
|
|
? getEffortThinkingLevelMap(source.reasoningOptions ?? [])
|
|
: undefined;
|
|
const result: ChatModel = { ...model, api, baseUrl, ...(thinkingLevelMap ? { thinkingLevelMap } : {}) };
|
|
if (compat) result.compat = compat;
|
|
// OpenCode variants list Claude Sonnet 4/4.5 with 1M context, actual limit is 200K.
|
|
if (model.id === "claude-sonnet-4-5" || model.id === "claude-sonnet-4") result.contextWindow = 200000;
|
|
if (["claude-opus-4-6", "claude-sonnet-4-6", "claude-opus-4.6", "claude-sonnet-4.6"].includes(model.id)) {
|
|
result.contextWindow = 1000000;
|
|
}
|
|
if (model.id === "gpt-5.4") {
|
|
result.contextWindow = 272000;
|
|
result.maxTokens = 128000;
|
|
}
|
|
return result;
|
|
},
|
|
});
|
|
}
|
|
|
|
export const OPENCODE_COMPLETIONS_COMPAT = {
|
|
supportsStore: false,
|
|
supportsDeveloperRole: false,
|
|
supportsStrictMode: true,
|
|
} as const;
|
|
|
|
/** Behavior shared by OpenCode Zen and Go. */
|
|
export function finalizeOpencodeModel(model: ChatModel): void {
|
|
if (model.provider === "opencode-go" && model.id === "glm-5.2") {
|
|
mergeThinking(model, { off: null, minimal: null, low: null, medium: null, high: "high", max: "max" });
|
|
}
|
|
if (model.provider === "opencode-go" && model.id === "kimi-k2.6") {
|
|
// OpenCode Go exposes Kimi K2.6 thinking as on/off, not distinct effort tiers.
|
|
mergeThinking(model, { minimal: null, low: null, medium: null });
|
|
}
|
|
if (model.provider === "opencode" && model.id === "grok-build-0.1") {
|
|
// OpenCode Zen Grok Build reasons by default but rejects explicit reasoningEffort.
|
|
mergeThinking(model, { off: null, minimal: null, low: null, medium: null });
|
|
}
|
|
if (model.api === "anthropic-messages") {
|
|
const messagesModel = model as Model<"anthropic-messages">;
|
|
// OpenCode Zen forwards mid-conversation system messages but rejects
|
|
// `tool_addition`/`tool_removal` blocks, so tool changes stay top-level.
|
|
if (model.provider === "opencode" && supportsClaudeMidConvoSystemMessages(model.id)) {
|
|
mergeCompat(messagesModel, { supportsMidConvoSystemMessages: true });
|
|
}
|
|
// OpenCode Qwen 3.8 Flash emits and accepts thinking blocks with empty signatures.
|
|
if (model.id === "qwen3.8-flash") mergeCompat(messagesModel, { allowEmptySignature: true });
|
|
}
|
|
// The Kimi K3 family accepts tool-bearing system messages; OpenCode passes them through.
|
|
if (model.api === "openai-completions" && isKimiK3FamilyId(model.id)) {
|
|
mergeCompat(model as Model<"openai-completions">, {
|
|
supportsMidConvoSystemMessages: true,
|
|
supportsMidConvoToolAdditions: true,
|
|
});
|
|
}
|
|
// OpenCode passes mid-conversation developer messages and `additional_tools`
|
|
// through to OpenAI unchanged; tool search is not verified through it.
|
|
enableOpenAiMidConvoSystemMessages(model, { proxy: true });
|
|
}
|
|
|
|
// OpenCode Zen serves Jev through its TypeSafe-compatible System One endpoint.
|
|
// Neither its /inference/openai/v1/models listing nor models.dev carries metadata for it.
|
|
// https://opencode.ai/docs/zen
|
|
function jev(id: string, name: string, input: number): ClassifierModel<"typesafe-system-one"> {
|
|
return {
|
|
type: "classifier",
|
|
id,
|
|
name,
|
|
api: "typesafe-system-one",
|
|
provider: "opencode",
|
|
baseUrl: "https://opencode.ai/inference/systemone/v1",
|
|
input: ["text"],
|
|
cost: { input, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 32000,
|
|
};
|
|
}
|
|
|
|
export default defineProvider({
|
|
id: "opencode",
|
|
models: opencodeModels,
|
|
classifiers: () => [jev("jev-1.13", "Jev 1.13", 0.042), jev("jev-1.13-free", "Jev 1.13 Free", 0)],
|
|
compat: { "openai-completions": OPENCODE_COMPLETIONS_COMPAT },
|
|
finalize: (model) => {
|
|
finalizeOpencodeModel(model);
|
|
enableOpenAiGrammarTools(model);
|
|
},
|
|
});
|