@linxiraos/pi-catalog 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +4 -2
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/build.d.ts +3 -2
- package/dist/types/compat/auth-ids.d.ts +2 -2
- package/dist/types/compat/axes.d.ts +1 -1
- package/dist/types/compat/behavior.d.ts +5 -0
- package/dist/types/compat/catalog-endpoint.d.ts +8 -0
- package/dist/types/compat/image-tokenization.d.ts +9 -1
- package/dist/types/compat/judgment-images.d.ts +25 -0
- package/dist/types/compat/provider-ids.d.ts +1 -1
- package/dist/types/compat/request-compression.d.ts +23 -0
- package/dist/types/compat/request-size.d.ts +12 -3
- package/dist/types/compat/thinking-replay.d.ts +9 -0
- package/dist/types/compat/types.d.ts +4 -0
- package/dist/types/discovery/antigravity.d.ts +7 -0
- package/dist/types/model-cache.d.ts +7 -0
- package/dist/types/model-manager.d.ts +6 -0
- package/dist/types/model-thinking.d.ts +11 -1
- package/dist/types/models.d.ts +16 -1
- package/dist/types/pricing.d.ts +37 -1
- package/dist/types/provider-models/openai-compat.d.ts +18 -46
- package/dist/types/types.d.ts +63 -12
- package/dist/types/wire/codex.d.ts +2 -0
- package/package.json +6 -6
- package/src/build.d.ts +3 -2
- package/src/build.ts +159 -80
- package/src/compat/auth-ids.ts +2 -0
- package/src/compat/axes.d.ts +1 -1
- package/src/compat/axes.ts +129 -4
- package/src/compat/behavior.d.ts +5 -0
- package/src/compat/behavior.ts +9 -1
- package/src/compat/cascade.ts +4 -1
- package/src/compat/catalog-endpoint.ts +43 -0
- package/src/compat/image-tokenization.ts +28 -10
- package/src/compat/judgment-images.ts +83 -0
- package/src/compat/provider-ids.d.ts +1 -1
- package/src/compat/provider-ids.ts +1 -0
- package/src/compat/request-compression.ts +43 -0
- package/src/compat/request-size.ts +20 -5
- package/src/compat/resolve.ts +90 -53
- package/src/compat/rules/README.md +32 -3
- package/src/compat/rules/auth/_order.kdl +1 -1
- package/src/compat/rules/auth/google-antigravity.kdl +1 -1
- package/src/compat/rules/auth/sambanova.kdl +9 -0
- package/src/compat/rules/auth/stencil.kdl +1 -1
- package/src/compat/rules/classes/anthropic.kdl +42 -2
- package/src/compat/rules/classes/deepseek.kdl +4 -0
- package/src/compat/rules/classes/gemini.kdl +46 -2
- package/src/compat/rules/classes/meta.kdl +7 -0
- package/src/compat/rules/classes/minimax.kdl +11 -1
- package/src/compat/rules/classes/openai.kdl +34 -3
- package/src/compat/rules/classes/xai.kdl +26 -0
- package/src/compat/rules/providers/amazon-bedrock.kdl +1 -0
- package/src/compat/rules/providers/anthropic.kdl +23 -0
- package/src/compat/rules/providers/azure.kdl +2 -0
- package/src/compat/rules/providers/cerebras.kdl +3 -0
- package/src/compat/rules/providers/cline-pass.kdl +16 -0
- package/src/compat/rules/providers/commandcode.kdl +10 -0
- package/src/compat/rules/providers/cursor.kdl +50 -1
- package/src/compat/rules/providers/deepinfra.kdl +3 -1
- package/src/compat/rules/providers/devin.kdl +117 -6
- package/src/compat/rules/providers/factory-droid.kdl +6 -0
- package/src/compat/rules/providers/firepass.kdl +15 -0
- package/src/compat/rules/providers/fireworks.kdl +26 -0
- package/src/compat/rules/providers/github-copilot.kdl +24 -0
- package/src/compat/rules/providers/google-antigravity.kdl +37 -0
- package/src/compat/rules/providers/google-gemini-cli.kdl +1 -0
- package/src/compat/rules/providers/google-vertex.kdl +5 -0
- package/src/compat/rules/providers/google.kdl +1 -0
- package/src/compat/rules/providers/image-tokenization.kdl +2 -1
- package/src/compat/rules/providers/local.kdl +8 -1
- package/src/compat/rules/providers/meta.kdl +16 -0
- package/src/compat/rules/providers/moonshot.kdl +2 -0
- package/src/compat/rules/providers/muse-code.kdl +49 -0
- package/src/compat/rules/providers/native-tools.kdl +3 -0
- package/src/compat/rules/providers/ollama-cloud.kdl +10 -0
- package/src/compat/rules/providers/openai-codex.kdl +29 -5
- package/src/compat/rules/providers/openai.kdl +1 -0
- package/src/compat/rules/providers/opencode-go.kdl +35 -0
- package/src/compat/rules/providers/opencode-zen.kdl +4 -0
- package/src/compat/rules/providers/openrouter.kdl +19 -0
- package/src/compat/rules/providers/sambanova.kdl +60 -0
- package/src/compat/rules/providers/tool-call-names.kdl +37 -0
- package/src/compat/rules/providers/typesafe.kdl +11 -0
- package/src/compat/rules/providers/umans.kdl +7 -0
- package/src/compat/rules/providers/venice.kdl +7 -0
- package/src/compat/rules/providers/xai-oauth.kdl +16 -6
- package/src/compat/rules/providers/xai.kdl +3 -0
- package/src/compat/rules/providers/zai.kdl +5 -0
- package/src/compat/rules/runtime/behavior.kdl +128 -8
- package/src/compat/rules/taxonomy/_collapse.kdl +11 -0
- package/src/compat/rules/taxonomy/gemma.kdl +1 -0
- package/src/compat/rules/taxonomy/meta.kdl +7 -0
- package/src/compat/rules/taxonomy/openai.kdl +2 -0
- package/src/compat/rules.json +1 -1
- package/src/compat/thinking-replay.ts +33 -0
- package/src/compat/types.d.ts +4 -0
- package/src/compat/types.ts +4 -0
- package/src/discovery/antigravity.d.ts +7 -0
- package/src/discovery/antigravity.ts +13 -0
- package/src/discovery/codex.ts +0 -4
- package/src/discovery/cursor.ts +28 -20
- package/src/discovery/devin.ts +7 -8
- package/src/model-cache.d.ts +7 -0
- package/src/model-cache.ts +8 -0
- package/src/model-manager.d.ts +6 -0
- package/src/model-manager.ts +41 -21
- package/src/model-thinking.d.ts +80 -0
- package/src/model-thinking.ts +13 -1
- package/src/model-tokenizer.ts +7 -1
- package/src/models.d.ts +16 -1
- package/src/models.json +1 -1
- package/src/models.ts +26 -12
- package/src/pricing.d.ts +37 -1
- package/src/pricing.ts +123 -12
- package/src/provider-models/bundled-references.ts +5 -2
- package/src/provider-models/cache-provider-id.ts +1 -1
- package/src/provider-models/descriptors.ts +2 -0
- package/src/provider-models/google.ts +7 -1
- package/src/provider-models/openai-compat.d.ts +18 -46
- package/src/provider-models/openai-compat.ts +188 -104
- package/src/provider-models/special.ts +1 -0
- package/src/types.d.ts +63 -12
- package/src/types.ts +71 -9
- package/src/wire/codex.d.ts +2 -0
- package/src/wire/codex.ts +17 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getBundledModels, isGeneratedProvider } from "../models";
|
|
2
|
+
import type { Model } from "../types";
|
|
3
|
+
import { resolveCatalogPolicy } from "./catalog-policy";
|
|
4
|
+
|
|
5
|
+
const bundledHosts = new Map<string, string | null>();
|
|
6
|
+
|
|
7
|
+
/** Hostname of the provider's bundled base URL; every provider bundles one origin. */
|
|
8
|
+
function bundledHost(provider: string): string | null {
|
|
9
|
+
let host = bundledHosts.get(provider);
|
|
10
|
+
if (host !== undefined) return host;
|
|
11
|
+
host = null;
|
|
12
|
+
if (isGeneratedProvider(provider)) {
|
|
13
|
+
for (const row of getBundledModels(provider)) {
|
|
14
|
+
try {
|
|
15
|
+
host = new URL(row.baseUrl).hostname;
|
|
16
|
+
break;
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
bundledHosts.set(provider, host);
|
|
21
|
+
return host;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether {@link url} is HTTPS to {@link model}'s provider's own endpoint: the
|
|
26
|
+
* host of its bundled base URL or one of its `endpoint-hosts`. A custom model
|
|
27
|
+
* or provider override that keeps a catalog provider id but points `baseUrl`
|
|
28
|
+
* at a gateway, proxy, or local server is not.
|
|
29
|
+
*/
|
|
30
|
+
export function isCatalogEndpointUrl(model: Model, url: string | undefined): boolean {
|
|
31
|
+
if (!url) return false;
|
|
32
|
+
let parsed: URL;
|
|
33
|
+
try {
|
|
34
|
+
parsed = new URL(url);
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (parsed.protocol !== "https:") return false;
|
|
39
|
+
if (parsed.hostname === bundledHost(model.provider)) return true;
|
|
40
|
+
// The rule compiler validates `endpoint-hosts` as an array of strings.
|
|
41
|
+
const hosts = resolveCatalogPolicy(model).endpointHosts as readonly string[] | undefined;
|
|
42
|
+
return hosts?.includes(parsed.hostname) === true;
|
|
43
|
+
}
|
|
@@ -37,12 +37,16 @@ export interface OpenAiPatchTokenization {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/** One model line's image billing rule: the vendor formula and its per-model numbers. */
|
|
40
|
-
export type ImageTokenization =
|
|
40
|
+
export type ImageTokenization = (
|
|
41
41
|
| OpenAiPatchTokenization
|
|
42
42
|
/** Anthropic 28px patches, resized to fit a padded-edge limit and a visual-token budget. */
|
|
43
43
|
| { regime: "anthropic-patch"; maxEdge: number; maxTokens: number }
|
|
44
44
|
/** A fixed per-image budget regardless of pixels (Gemini 3 `media_resolution`). */
|
|
45
|
-
| { regime: "fixed"; tokens: number }
|
|
45
|
+
| { regime: "fixed"; tokens: number }
|
|
46
|
+
) & {
|
|
47
|
+
/** Provider billing was measured to match this rule; unset for docs-only rules and wire fallbacks. */
|
|
48
|
+
verified?: boolean;
|
|
49
|
+
};
|
|
46
50
|
|
|
47
51
|
/** A `class: "unknown"` identity for targets resolved by wire API alone. */
|
|
48
52
|
const UNCLASSIFIED: Pick<ModelIdentity, "class"> = { class: "unknown" };
|
|
@@ -62,17 +66,13 @@ export interface ImageTokenizationTarget {
|
|
|
62
66
|
identity?: Pick<ModelIdentity, "class" | "family" | "revision">;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
* fallback; undefined when neither covers it (an unclassified model on an
|
|
68
|
-
* unregistered API).
|
|
69
|
-
*/
|
|
70
|
-
export function resolveImageTokenization(target: ImageTokenizationTarget): ImageTokenization | undefined {
|
|
69
|
+
/** Catalog axes for `target`'s identity on its host and wire API. */
|
|
70
|
+
function resolveImageAxes(target: ImageTokenizationTarget): Record<string, unknown> {
|
|
71
71
|
const provider = target.provider ?? "";
|
|
72
72
|
const model = target.id ?? "";
|
|
73
73
|
const identity: Pick<ModelIdentity, "class" | "family" | "revision"> =
|
|
74
74
|
target.identity ?? (model ? classifyModel(provider, model, { lenient: true }) : UNCLASSIFIED);
|
|
75
|
-
|
|
75
|
+
return resolveCascade({
|
|
76
76
|
provider,
|
|
77
77
|
api: target.api ?? "",
|
|
78
78
|
class: identity.class,
|
|
@@ -80,10 +80,28 @@ export function resolveImageTokenization(target: ImageTokenizationTarget): Image
|
|
|
80
80
|
reasoning: false,
|
|
81
81
|
...(identity.family !== undefined && { family: identity.family }),
|
|
82
82
|
...(identity.revision !== undefined && { revision: identity.revision }),
|
|
83
|
-
}).catalog
|
|
83
|
+
}).catalog;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The reader's image billing rule: its lineage rule, else its wire API's
|
|
88
|
+
* fallback; undefined when neither covers it (an unclassified model on an
|
|
89
|
+
* unregistered API).
|
|
90
|
+
*/
|
|
91
|
+
export function resolveImageTokenization(target: ImageTokenizationTarget): ImageTokenization | undefined {
|
|
92
|
+
const rule = resolveImageAxes(target).imageTokenization;
|
|
84
93
|
return isImageTokenization(rule) ? rule : undefined;
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Longest image edge the reader's host passes to the model (`image-max-edge`),
|
|
98
|
+
* or undefined when the host keeps the image's size.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveImageMaxEdge(target: ImageTokenizationTarget): number | undefined {
|
|
101
|
+
const edge = resolveImageAxes(target).imageMaxEdge;
|
|
102
|
+
return typeof edge === "number" && edge > 0 ? edge : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
/** Scale `size` down (never up) so its longest side is at most `maxEdge`. */
|
|
88
106
|
function fitLongEdge(size: ImageSize, maxEdge: number): ImageSize {
|
|
89
107
|
const longest = Math.max(size.width, size.height);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isRecord } from "../utils";
|
|
2
|
+
import type { Model } from "../types";
|
|
3
|
+
import { resolveCatalogPolicy } from "./catalog-policy";
|
|
4
|
+
|
|
5
|
+
/** Bounds a judgment model applies to image inputs. */
|
|
6
|
+
export interface JudgmentImageInput {
|
|
7
|
+
/** Accepted lowercase MIME types; unset adds no model-specific format restriction. */
|
|
8
|
+
mimeTypes?: readonly string[];
|
|
9
|
+
/** Images per request. */
|
|
10
|
+
maxCount?: number;
|
|
11
|
+
/** Decoded bytes per image. */
|
|
12
|
+
maxBytes?: number;
|
|
13
|
+
/** Decoded bytes across all images. */
|
|
14
|
+
maxTotalBytes?: number;
|
|
15
|
+
/** Pixels per image, when the image header reports dimensions. */
|
|
16
|
+
maxPixels?: number;
|
|
17
|
+
/** Minimum pixels per image, when the image header reports dimensions. */
|
|
18
|
+
minPixels?: number;
|
|
19
|
+
/** Inclusive bounds on each side, when the image header reports dimensions. */
|
|
20
|
+
minSide?: number;
|
|
21
|
+
maxSide?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type NumericField = "maxCount" | "maxBytes" | "maxTotalBytes" | "maxPixels" | "minPixels" | "minSide" | "maxSide";
|
|
25
|
+
|
|
26
|
+
const NUMERIC_FIELDS: readonly NumericField[] = [
|
|
27
|
+
"maxCount",
|
|
28
|
+
"maxBytes",
|
|
29
|
+
"maxTotalBytes",
|
|
30
|
+
"maxPixels",
|
|
31
|
+
"minPixels",
|
|
32
|
+
"minSide",
|
|
33
|
+
"maxSide",
|
|
34
|
+
];
|
|
35
|
+
const POLICY_FIELDS: Record<string, true> = {
|
|
36
|
+
mimeTypes: true,
|
|
37
|
+
maxCount: true,
|
|
38
|
+
maxBytes: true,
|
|
39
|
+
maxTotalBytes: true,
|
|
40
|
+
maxPixels: true,
|
|
41
|
+
minPixels: true,
|
|
42
|
+
minSide: true,
|
|
43
|
+
maxSide: true,
|
|
44
|
+
};
|
|
45
|
+
const MIME_TYPE = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/;
|
|
46
|
+
|
|
47
|
+
function positiveInteger(value: unknown): value is number {
|
|
48
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve image bounds assigned to this model by the catalog cascade. KDL
|
|
53
|
+
* object payloads have scalar children, so `mime-types` is authored as a
|
|
54
|
+
* comma-separated string and normalized here to the public array type.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveJudgmentImageInput(model: Model): JudgmentImageInput | undefined {
|
|
57
|
+
const value = resolveCatalogPolicy(model).judgmentImageInput;
|
|
58
|
+
if (!isRecord(value) || Object.keys(value).some(key => !Object.hasOwn(POLICY_FIELDS, key))) return undefined;
|
|
59
|
+
|
|
60
|
+
let mimeTypes: readonly string[] | undefined;
|
|
61
|
+
if (value.mimeTypes !== undefined) {
|
|
62
|
+
if (typeof value.mimeTypes !== "string") return undefined;
|
|
63
|
+
const parsed = value.mimeTypes.split(",");
|
|
64
|
+
if (parsed.some(mimeType => !MIME_TYPE.test(mimeType))) return undefined;
|
|
65
|
+
mimeTypes = Object.freeze(parsed);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const numeric: Partial<Record<NumericField, number>> = {};
|
|
69
|
+
for (const field of NUMERIC_FIELDS) {
|
|
70
|
+
const bound = value[field];
|
|
71
|
+
if (bound === undefined) continue;
|
|
72
|
+
if (!positiveInteger(bound)) return undefined;
|
|
73
|
+
numeric[field] = bound;
|
|
74
|
+
}
|
|
75
|
+
if (
|
|
76
|
+
(numeric.minPixels !== undefined && numeric.maxPixels !== undefined && numeric.minPixels > numeric.maxPixels) ||
|
|
77
|
+
(numeric.minSide !== undefined && numeric.maxSide !== undefined && numeric.minSide > numeric.maxSide)
|
|
78
|
+
) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { ...(mimeTypes !== undefined && { mimeTypes }), ...numeric };
|
|
83
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Every chat-model provider with a catalog entry (`default-model` in its provider rules). */
|
|
2
|
-
export type KnownProvider = "abliteration" | "aiand" | "aimlapi" | "alibaba-coding-plan" | "alibaba-token-plan" | "amazon-bedrock" | "anthropic" | "apple" | "azure" | "baseten" | "bedrock-mantle" | "cerebras" | "charm-hyper" | "cline-pass" | "cloudflare-ai-gateway" | "cloudflare-workers-ai" | "commandcode" | "coralbricks" | "coreweave" | "cursor" | "deepinfra" | "deepseek" | "devin" | "factory-droid" | "firepass" | "fireworks" | "github-copilot" | "gitlab-duo" | "gitlab-duo-agent" | "gmi-cloud" | "google" | "google-antigravity" | "google-gemini-cli" | "google-vertex" | "groq" | "helmcode" | "huggingface" | "kilo" | "kimi-code" | "litellm" | "lm-studio" | "local" | "meta" | "minimax" | "minimax-code" | "minimax-code-cn" | "mistral" | "moonshot" | "muse-code" | "nanogpt" | "novita" | "nvidia" | "ollama" | "ollama-cloud" | "openai" | "openai-codex" | "opencode-go" | "opencode-zen" | "openrouter" | "qianfan" | "qwen-portal" | "sakana" | "siliconflow" | "siliconflow-cn" | "singularityapi-dev" | "singularityapi-tech" | "snowflake" | "stepfun" | "synthetic" | "together" | "typesafe" | "umans" | "venice" | "vercel-ai-gateway" | "vllm" | "wafer-serverless" | "web" | "xai" | "xai-oauth" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "yolo-auto" | "zai" | "zenmux" | "zhipu-coding-plan";
|
|
2
|
+
export type KnownProvider = "abliteration" | "aiand" | "aimlapi" | "alibaba-coding-plan" | "alibaba-token-plan" | "amazon-bedrock" | "anthropic" | "apple" | "azure" | "baseten" | "bedrock-mantle" | "cerebras" | "charm-hyper" | "cline-pass" | "cloudflare-ai-gateway" | "cloudflare-workers-ai" | "commandcode" | "coralbricks" | "coreweave" | "cursor" | "deepinfra" | "deepseek" | "devin" | "factory-droid" | "firepass" | "fireworks" | "github-copilot" | "gitlab-duo" | "gitlab-duo-agent" | "gmi-cloud" | "google" | "google-antigravity" | "google-gemini-cli" | "google-vertex" | "groq" | "helmcode" | "huggingface" | "kilo" | "kimi-code" | "litellm" | "lm-studio" | "local" | "meta" | "minimax" | "minimax-code" | "minimax-code-cn" | "mistral" | "moonshot" | "muse-code" | "nanogpt" | "novita" | "nvidia" | "ollama" | "ollama-cloud" | "openai" | "openai-codex" | "opencode-go" | "opencode-zen" | "openrouter" | "qianfan" | "qwen-portal" | "sakana" | "sambanova" | "siliconflow" | "siliconflow-cn" | "singularityapi-dev" | "singularityapi-tech" | "snowflake" | "stepfun" | "synthetic" | "together" | "typesafe" | "umans" | "venice" | "vercel-ai-gateway" | "vllm" | "wafer-serverless" | "web" | "xai" | "xai-oauth" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "yolo-auto" | "zai" | "zenmux" | "zhipu-coding-plan";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Model } from "../types";
|
|
2
|
+
import { isCatalogEndpointUrl } from "./catalog-endpoint";
|
|
3
|
+
import { resolveCatalogPolicy } from "./catalog-policy";
|
|
4
|
+
|
|
5
|
+
/** `Content-Encoding` values a host may accept on request bodies. */
|
|
6
|
+
export type RequestBodyEncoding = "zstd" | "gzip";
|
|
7
|
+
|
|
8
|
+
/** Request-body compression contract for one request. */
|
|
9
|
+
export interface RequestCompressionPolicy {
|
|
10
|
+
/** Accepted encodings, most preferred first. */
|
|
11
|
+
readonly encodings: readonly RequestBodyEncoding[];
|
|
12
|
+
/**
|
|
13
|
+
* Pattern of a JSON 400/415 error body that rejects the encoding;
|
|
14
|
+
* `undefined` treats every JSON 400/415 as a possible rejection. A
|
|
15
|
+
* 400/403/415 whose body is not JSON (presumed a proxy's answer, not the
|
|
16
|
+
* host's) is a possible rejection either way.
|
|
17
|
+
*/
|
|
18
|
+
readonly rejection: RegExp | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const rejectionPatterns = new Map<string, RegExp>();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Compression contract for a request from {@link model} to {@link url}, or
|
|
25
|
+
* `undefined` when the body must go uncompressed. The `request-encodings` axis
|
|
26
|
+
* applies only while the URL reaches the provider's own endpoint
|
|
27
|
+
* ({@link isCatalogEndpointUrl}): a proxy, relay, or rerouted base URL never
|
|
28
|
+
* receives bytes it cannot read.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveRequestCompression(model: Model, url: string): RequestCompressionPolicy | undefined {
|
|
31
|
+
const policy = resolveCatalogPolicy(model);
|
|
32
|
+
// The rule compiler validates encodings against the axis vocabulary.
|
|
33
|
+
const encodings = policy.requestEncodings as readonly RequestBodyEncoding[] | undefined;
|
|
34
|
+
if (!encodings?.length || !isCatalogEndpointUrl(model, url)) return undefined;
|
|
35
|
+
const source = policy.requestEncodingRejection;
|
|
36
|
+
if (typeof source !== "string") return { encodings, rejection: undefined };
|
|
37
|
+
let rejection = rejectionPatterns.get(source);
|
|
38
|
+
if (!rejection) {
|
|
39
|
+
rejection = new RegExp(source);
|
|
40
|
+
rejectionPatterns.set(source, rejection);
|
|
41
|
+
}
|
|
42
|
+
return { encodings, rejection };
|
|
43
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toModelSpec } from "../provider-models/bundled-references";
|
|
2
2
|
import type { Model } from "../types";
|
|
3
|
+
import { resolveCatalogPolicy } from "./catalog-policy";
|
|
3
4
|
import { resolveCatalogAxes } from "./resolve";
|
|
4
5
|
|
|
5
6
|
const imageBudgetCache = new Map<string, number | null>();
|
|
@@ -7,12 +8,12 @@ const IMAGE_BUDGET_CACHE_MAX = 8192;
|
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Maximum encoded inline-image bytes retained per request for a deployment with a documented body-size limit.
|
|
10
|
-
* The axis is a host contract: a rerouted
|
|
11
|
-
*
|
|
11
|
+
* The axis is a host contract: a rerouted endpoint (proxy, gateway) imposes its own limit, so the budget applies
|
|
12
|
+
* only when `officialEndpoint` says the request reaches the model's official endpoint. `compat.officialEndpoint`
|
|
13
|
+
* classifies only the configured `baseUrl`; pi-ai's `reachesOfficialEndpoint` also follows request-time reroutes.
|
|
12
14
|
*/
|
|
13
|
-
export function resolveInlineImageByteBudget(model: Model): number | undefined {
|
|
14
|
-
|
|
15
|
-
if (!compat || !("officialEndpoint" in compat) || !compat.officialEndpoint) return undefined;
|
|
15
|
+
export function resolveInlineImageByteBudget(model: Model, officialEndpoint: boolean): number | undefined {
|
|
16
|
+
if (!officialEndpoint) return undefined;
|
|
16
17
|
const key = `${model.provider} ${model.id} ${model.api}`;
|
|
17
18
|
let budget = imageBudgetCache.get(key);
|
|
18
19
|
if (budget === undefined) {
|
|
@@ -23,3 +24,17 @@ export function resolveInlineImageByteBudget(model: Model): number | undefined {
|
|
|
23
24
|
}
|
|
24
25
|
return budget ?? undefined;
|
|
25
26
|
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Most images one request to this deployment may carry, or `undefined` when no rule names the host. A configured
|
|
30
|
+
* `model.imageCountBudget` wins: the user knows the host behind a custom gateway. Otherwise the
|
|
31
|
+
* `official-image-count-budget` axis replaces `image-count-budget` only when `officialEndpoint` says the request
|
|
32
|
+
* reaches the model's official endpoint (see {@link resolveInlineImageByteBudget}); a rerouted endpoint keeps the
|
|
33
|
+
* provider's generic budget.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveImageCountBudget(model: Model, officialEndpoint: boolean): number | undefined {
|
|
36
|
+
if (model.imageCountBudget !== undefined) return model.imageCountBudget;
|
|
37
|
+
const policy = resolveCatalogPolicy(model);
|
|
38
|
+
const value = (officialEndpoint && policy.officialImageCountBudget) || policy.imageCountBudget;
|
|
39
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
40
|
+
}
|
package/src/compat/resolve.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { Effort, THINKING_EFFORTS } from "../effort";
|
|
19
19
|
import { hostMatchesUrl, modelMatchesHost } from "../hosts";
|
|
20
|
+
import { withoutUnverifiedThinking } from "../model-thinking";
|
|
20
21
|
import type {
|
|
21
22
|
Api,
|
|
22
23
|
CompatOf,
|
|
@@ -26,11 +27,13 @@ import type {
|
|
|
26
27
|
ResolvedBedrockCompat,
|
|
27
28
|
ResolvedDevinCompat,
|
|
28
29
|
ResolvedGoogleCompat,
|
|
30
|
+
ResolvedOllamaCompat,
|
|
29
31
|
ResolvedOpenAICompat,
|
|
30
32
|
ResolvedOpenAIResponsesCompat,
|
|
31
33
|
ResolvedOpenAISharedCompat,
|
|
32
34
|
ThinkingConfig,
|
|
33
35
|
} from "../types";
|
|
36
|
+
import { isOfficialCodexApiUrl } from "../wire/codex";
|
|
34
37
|
import {
|
|
35
38
|
isAnthropicSigningProxyUrl,
|
|
36
39
|
isAzureAnthropicRoute,
|
|
@@ -117,11 +120,24 @@ class IdentityFacts {
|
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The id a spec resolves identity and rules under: its own, unless that names
|
|
125
|
+
* no model and the row sends a model that does on the wire. A Devin Fusion
|
|
126
|
+
* pairing runs only its lead, so the lead's reviewed rules (cache-write rate,
|
|
127
|
+
* long-context tier) apply to it at build time.
|
|
128
|
+
*/
|
|
129
|
+
function policyModelId<TApi extends Api>(spec: ModelSpec<TApi>): string {
|
|
130
|
+
const wireId = spec.requestModelId;
|
|
131
|
+
if (wireId === undefined || wireId === spec.id) return spec.id;
|
|
132
|
+
if (classifyModel(spec.provider, spec.id).class !== "unknown") return spec.id;
|
|
133
|
+
return classifyModel(spec.provider, wireId).class === "unknown" ? spec.id : wireId;
|
|
134
|
+
}
|
|
135
|
+
|
|
120
136
|
function resolveIdentity<TApi extends Api>(spec: ModelSpec<TApi>): ModelIdentity {
|
|
121
137
|
// Strict on purpose: ambiguous identity is a rule-authoring defect surfaced
|
|
122
138
|
// at build/CI time, never silently degraded to `unknown`. Discovery
|
|
123
139
|
// normalization opts into leniency through `classifyModel` directly.
|
|
124
|
-
return classifyModel(spec.provider, spec
|
|
140
|
+
return classifyModel(spec.provider, policyModelId(spec));
|
|
125
141
|
}
|
|
126
142
|
|
|
127
143
|
// ---------------------------------------------------------------------------
|
|
@@ -261,6 +277,7 @@ function hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {
|
|
|
261
277
|
let hostname: string;
|
|
262
278
|
try {
|
|
263
279
|
hostname = new URL(baseUrl).hostname.toLowerCase();
|
|
280
|
+
if (hostname.endsWith(".")) hostname = hostname.slice(0, -1);
|
|
264
281
|
} catch {
|
|
265
282
|
return false;
|
|
266
283
|
}
|
|
@@ -313,7 +330,6 @@ function isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {
|
|
|
313
330
|
|
|
314
331
|
interface OpenAIDetection {
|
|
315
332
|
facts: IdentityFacts;
|
|
316
|
-
isClinePass: boolean;
|
|
317
333
|
isZai: boolean;
|
|
318
334
|
isZhipu: boolean;
|
|
319
335
|
isMoonshotNative: boolean;
|
|
@@ -351,7 +367,6 @@ function detectOpenAI(
|
|
|
351
367
|
(LOCAL_OPENAI_COMPAT_PROVIDERS[backendProvider] === true || hasLocalLoopbackBaseUrl(baseUrl));
|
|
352
368
|
return {
|
|
353
369
|
facts,
|
|
354
|
-
isClinePass: provider === "cline-pass",
|
|
355
370
|
isZai,
|
|
356
371
|
isZhipu,
|
|
357
372
|
isMoonshotNative,
|
|
@@ -457,7 +472,9 @@ function detectOpenAICompat(
|
|
|
457
472
|
provider === "github-copilot" ||
|
|
458
473
|
provider === "zenmux");
|
|
459
474
|
|
|
460
|
-
//
|
|
475
|
+
// URL-keyed watchdogs: they also cover custom provider ids pointed at these
|
|
476
|
+
// hosts, which provider-keyed rules (`stream-idle-timeout-ms` in
|
|
477
|
+
// classes/{glm,kimi,mimo}.kdl) cannot match. KDL rules override them.
|
|
461
478
|
const streamIdleTimeoutMs =
|
|
462
479
|
facts.is("glm") &&
|
|
463
480
|
facts.revGte("5") &&
|
|
@@ -504,21 +521,17 @@ function detectOpenAICompat(
|
|
|
504
521
|
supportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,
|
|
505
522
|
supportsReasoningEffort: !isGrok && !d.isXiaomiMimo && (!(d.isZai || d.isZhipu) || supportsZaiReasoningEffort),
|
|
506
523
|
trustExplicitThinkingOnly: undefined,
|
|
507
|
-
|
|
508
|
-
// provider rule without changing Copilot Responses rows.
|
|
509
|
-
supportsReasoningParams: provider !== "github-copilot",
|
|
524
|
+
supportsReasoningParams: true,
|
|
510
525
|
supportsSamplingParams: true,
|
|
511
526
|
supportsPenaltyAndStopParams: !(isGrok && reasoningCapable),
|
|
512
527
|
reasoningEffortMap: {},
|
|
513
528
|
supportsUsageInStreaming: !isCerebrasHost,
|
|
514
529
|
alwaysSendMaxTokens: facts.is("kimi"),
|
|
515
|
-
disableReasoningOnForcedToolChoice:
|
|
516
|
-
|
|
517
|
-
disableReasoningOnToolChoice: !d.isClinePass && isDeepseekFamily && reasoningCapable && !d.isOpenRouter,
|
|
530
|
+
disableReasoningOnForcedToolChoice: (facts.is("kimi") && !isMoonshotKimiK3) || isAnthropicModel,
|
|
531
|
+
disableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !d.isOpenRouter,
|
|
518
532
|
disableReasoningWithTools: false,
|
|
519
|
-
supportsToolChoice:
|
|
520
|
-
supportsForcedToolChoice:
|
|
521
|
-
!d.requiresEnabledThinking && !(d.isOpenCodeHost && d.isDeepseekReasoning) && !(d.isClinePass && isQwen),
|
|
533
|
+
supportsToolChoice: !d.isDirectDeepseekReasoning,
|
|
534
|
+
supportsForcedToolChoice: !d.requiresEnabledThinking && !(d.isOpenCodeHost && d.isDeepseekReasoning),
|
|
522
535
|
supportsNamedToolChoice: true,
|
|
523
536
|
maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
|
|
524
537
|
requiresToolResultName: isMistral,
|
|
@@ -527,16 +540,12 @@ function detectOpenAICompat(
|
|
|
527
540
|
requiresMistralToolIds: isMistral,
|
|
528
541
|
thinkingFormat,
|
|
529
542
|
kimiApiFormat: undefined,
|
|
530
|
-
reasoningDisableMode: d.
|
|
531
|
-
? "cline-enabled-false"
|
|
532
|
-
: d.isVenice
|
|
533
|
-
? "venice-disable-thinking"
|
|
534
|
-
: resolveReasoningDisableMode(thinkingFormat),
|
|
543
|
+
reasoningDisableMode: d.isVenice ? "venice-disable-thinking" : resolveReasoningDisableMode(thinkingFormat),
|
|
535
544
|
omitReasoningEffort: false,
|
|
536
545
|
includeEncryptedReasoning: true,
|
|
537
546
|
filterReasoningHistory: d.isOpenRouter && isAnthropicModel,
|
|
538
547
|
thinkingKeep: usesMoonshotKimiPreservedThinking ? "all" : undefined,
|
|
539
|
-
reasoningContentField:
|
|
548
|
+
reasoningContentField: "reasoning_content",
|
|
540
549
|
mistralReasoningContentParts: undefined,
|
|
541
550
|
requiresReasoningContentForToolCalls:
|
|
542
551
|
(facts.is("kimi") && !d.isOpenCodeProvider) ||
|
|
@@ -554,10 +563,7 @@ function detectOpenAICompat(
|
|
|
554
563
|
// Template effort support is a reviewed backend × model contract in KDL.
|
|
555
564
|
qwenTemplateReasoningEffort: false,
|
|
556
565
|
requiresAssistantContentForToolCalls: facts.is("kimi") || d.isDirectDeepseekReasoning,
|
|
557
|
-
cacheControlFormat:
|
|
558
|
-
(d.isClinePass && (isQwen || isAnthropicModel)) || (d.isOpenRouter && isAnthropicModel)
|
|
559
|
-
? "anthropic"
|
|
560
|
-
: undefined,
|
|
566
|
+
cacheControlFormat: d.isOpenRouter && isAnthropicModel ? "anthropic" : undefined,
|
|
561
567
|
supportsPromptCacheBreakpoints,
|
|
562
568
|
promptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? "30m" : undefined,
|
|
563
569
|
openRouterRouting: undefined,
|
|
@@ -582,7 +588,11 @@ function detectOpenAICompat(
|
|
|
582
588
|
: undefined,
|
|
583
589
|
streamFirstEventTimeoutMs: d.isLocalServingBackend ? 0 : undefined,
|
|
584
590
|
streamIdleTimeoutMs,
|
|
585
|
-
stripDeepseekSpecialTokens:
|
|
591
|
+
stripDeepseekSpecialTokens: false,
|
|
592
|
+
// Present as a key so models.yml `compat` can set it; unset unless a rule assigns it.
|
|
593
|
+
stripThinkingHistory: undefined,
|
|
594
|
+
// URL-derived like the Responses field; unset off the official OpenAI API.
|
|
595
|
+
officialEndpoint: isOfficialOpenAIEndpoint(spec.provider, baseUrl) || undefined,
|
|
586
596
|
streamMarkupHealingPattern: detectStreamMarkupHealing(spec.provider, facts, baseUrl),
|
|
587
597
|
reasoningDeltasMayBeCumulative: false,
|
|
588
598
|
emptyLengthFinishIsContextError: false,
|
|
@@ -640,15 +650,13 @@ function fixupOpenAICompat(
|
|
|
640
650
|
compat.extraBody = Object.keys(extraBody).length > 0 ? extraBody : undefined;
|
|
641
651
|
}
|
|
642
652
|
if (spec.compat?.reasoningDisableMode === undefined && !("reasoningDisableMode" in axes.wire)) {
|
|
643
|
-
compat.reasoningDisableMode = d.
|
|
644
|
-
? "
|
|
645
|
-
: d.
|
|
646
|
-
? "
|
|
647
|
-
: d.
|
|
648
|
-
? "
|
|
649
|
-
:
|
|
650
|
-
? "venice-disable-thinking"
|
|
651
|
-
: resolveReasoningDisableMode(compat.thinkingFormat);
|
|
653
|
+
compat.reasoningDisableMode = d.requiresEnabledThinking
|
|
654
|
+
? "omit"
|
|
655
|
+
: d.isDirectDeepseekReasoning
|
|
656
|
+
? "zai-thinking-disabled"
|
|
657
|
+
: d.isVenice
|
|
658
|
+
? "venice-disable-thinking"
|
|
659
|
+
: resolveReasoningDisableMode(compat.thinkingFormat);
|
|
652
660
|
}
|
|
653
661
|
if (
|
|
654
662
|
spec.compat?.omitReasoningEffort === undefined &&
|
|
@@ -737,7 +745,8 @@ function resolveOpenAIResponsesPolicy(
|
|
|
737
745
|
supportsLongPromptCacheRetention: isOpenAIUrl,
|
|
738
746
|
supportsPromptCacheBreakpoints,
|
|
739
747
|
promptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? "30m" : undefined,
|
|
740
|
-
|
|
748
|
+
// Provider ids are rule-owned; the URL arm keeps custom Azure endpoints paired.
|
|
749
|
+
strictResponsesPairing: hostMatchesUrl(baseUrl, "azureOpenAI"),
|
|
741
750
|
// Azure's provider id alone only implies support while its endpoint is
|
|
742
751
|
// resolved at runtime; an explicit non-Azure baseUrl is a proxy, like Codex.
|
|
743
752
|
supportsImageDetailOriginal:
|
|
@@ -766,7 +775,7 @@ function resolveOpenAIResponsesPolicy(
|
|
|
766
775
|
disableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,
|
|
767
776
|
disableReasoningWithTools: false,
|
|
768
777
|
supportsToolChoice: true,
|
|
769
|
-
supportsForcedToolChoice:
|
|
778
|
+
supportsForcedToolChoice: true,
|
|
770
779
|
supportsNamedToolChoice: true,
|
|
771
780
|
reasoningContentField: "reasoning_content",
|
|
772
781
|
requiresReasoningContentForToolCalls:
|
|
@@ -796,12 +805,16 @@ function resolveOpenAIResponsesPolicy(
|
|
|
796
805
|
supportsObfuscationOptOut: isOpenAIUrl || provider === "openai",
|
|
797
806
|
storeResponses: false,
|
|
798
807
|
connectionBoundNativeHistory: false,
|
|
799
|
-
|
|
808
|
+
// The Codex backend is official at the URL its requests default to when `baseUrl` is unset, and at paths
|
|
809
|
+
// under it such as `/codex` and `/codex/responses`, which its requests resolve to the same endpoint.
|
|
810
|
+
officialEndpoint:
|
|
811
|
+
isOfficialOpenAIEndpoint(provider, baseUrl) ||
|
|
812
|
+
(api === "openai-codex-responses" && isOfficialCodexApiUrl(baseUrl)),
|
|
800
813
|
harmonyLeakMitigation: false,
|
|
801
814
|
rejectRootObjectUnion: false,
|
|
802
815
|
retryWithoutStrictOnGrammarError: false,
|
|
803
816
|
cacheControlFormat: isOpenRouter && isAnthropicModel ? "anthropic" : undefined,
|
|
804
|
-
stripDeepseekSpecialTokens:
|
|
817
|
+
stripDeepseekSpecialTokens: false,
|
|
805
818
|
streamMarkupHealingPattern: detectStreamMarkupHealing(provider, facts, baseUrl),
|
|
806
819
|
reasoningDeltasMayBeCumulative: false,
|
|
807
820
|
emptyLengthFinishIsContextError: false,
|
|
@@ -892,6 +905,7 @@ function resolveAnthropicPolicy(
|
|
|
892
905
|
supportsContextManagement: true,
|
|
893
906
|
supportsServerCompaction: false,
|
|
894
907
|
firstPartyProvider: false,
|
|
908
|
+
honorsClaudeCodeBetaKillSwitch: false,
|
|
895
909
|
supportsOutputEffort: true,
|
|
896
910
|
disableStrictTools: rejectsStrictTools,
|
|
897
911
|
// Present as a key so models.yml `compat` can set it; unset unless detected.
|
|
@@ -953,9 +967,17 @@ function resolveBedrockPolicy(spec: ModelSpec<"bedrock-converse-stream">, axes:
|
|
|
953
967
|
return compat;
|
|
954
968
|
}
|
|
955
969
|
|
|
970
|
+
function resolveOllamaPolicy(spec: ModelSpec<"ollama-chat">, axes: ResolvedAxes): ResolvedOllamaCompat {
|
|
971
|
+
const compat: ResolvedOllamaCompat = { stripThinkingHistory: false };
|
|
972
|
+
applyWireAxes(compat, axes.wire, "ollama-chat");
|
|
973
|
+
applyCompatOverrides(compat, spec.compat);
|
|
974
|
+
return compat;
|
|
975
|
+
}
|
|
976
|
+
|
|
956
977
|
function resolveDevinPolicy(spec: ModelSpec<"devin-agent">, axes: ResolvedAxes): ResolvedDevinCompat {
|
|
957
978
|
const compat: ResolvedDevinCompat = {
|
|
958
|
-
|
|
979
|
+
// Rule-owned: `on-api "devin-agent"` in providers/devin.kdl opts in.
|
|
980
|
+
trustExplicitThinkingOnly: false,
|
|
959
981
|
modelRouter: false,
|
|
960
982
|
supportsParallelToolCalls: false,
|
|
961
983
|
};
|
|
@@ -979,6 +1001,10 @@ function resolveGooglePolicy(
|
|
|
979
1001
|
claudeThinkingBetaHeader: false,
|
|
980
1002
|
antigravityClaudeToolMode: false,
|
|
981
1003
|
stripImageInput: false,
|
|
1004
|
+
// Unset means "no image contract": images go out as sent. Keys exist so
|
|
1005
|
+
// a KDL rule or a user `compat` block can declare one.
|
|
1006
|
+
maxImageDimension: undefined,
|
|
1007
|
+
maxImagePayloadBytes: undefined,
|
|
982
1008
|
};
|
|
983
1009
|
applyWireAxes(compat, axes.wire, spec.api);
|
|
984
1010
|
applyCompatOverrides(compat, spec.compat);
|
|
@@ -1004,7 +1030,11 @@ function readCompatEffortMap(compat: CompatOf<Api>): Partial<Record<Effort, stri
|
|
|
1004
1030
|
const map = compat.reasoningEffortMap;
|
|
1005
1031
|
return map && Object.keys(map).length > 0 ? map : undefined;
|
|
1006
1032
|
}
|
|
1007
|
-
/**
|
|
1033
|
+
/**
|
|
1034
|
+
* URL-keyed Fireworks effort remap: covers custom provider ids pointed at the
|
|
1035
|
+
* Fireworks host, which the `fireworks`/`firepass` `thinking-effort-map`
|
|
1036
|
+
* rules cannot match. Rule and configured maps override it.
|
|
1037
|
+
*/
|
|
1008
1038
|
const FIREWORKS_THINKING_EFFORT_MAP: Readonly<Partial<Record<Effort, string>>> = {
|
|
1009
1039
|
[Effort.Minimal]: "none",
|
|
1010
1040
|
};
|
|
@@ -1015,12 +1045,9 @@ function defaultThinkingMode<TApi extends Api>(spec: ModelSpec<TApi>, facts: Ide
|
|
|
1015
1045
|
case "google-generative-ai":
|
|
1016
1046
|
case "google-gemini-cli":
|
|
1017
1047
|
case "google-vertex":
|
|
1018
|
-
|
|
1048
|
+
// Gemini 3+ `google-level` is a `thinking-mode` rule in classes/gemini.kdl.
|
|
1049
|
+
return "budget";
|
|
1019
1050
|
case "anthropic-messages":
|
|
1020
|
-
if (facts.is("minimax") && facts.family("m2", "m3")) return "anthropic-adaptive";
|
|
1021
|
-
if (facts.is("glm") && facts.revGte("5.2") && (spec.provider === "umans" || spec.provider === "zai")) {
|
|
1022
|
-
return "anthropic-budget-effort";
|
|
1023
|
-
}
|
|
1024
1051
|
if (facts.is("anthropic")) {
|
|
1025
1052
|
// Haiku stays on budget thinking until 5.5, its first adaptive generation.
|
|
1026
1053
|
if (facts.revGte(facts.family("haiku") ? "5.5" : "4.6")) return "anthropic-adaptive";
|
|
@@ -1088,6 +1115,7 @@ interface RuleThinking {
|
|
|
1088
1115
|
supportsDisplay?: boolean;
|
|
1089
1116
|
prefixBinding?: boolean;
|
|
1090
1117
|
upgradeNeutral?: boolean;
|
|
1118
|
+
unverified?: boolean;
|
|
1091
1119
|
}
|
|
1092
1120
|
|
|
1093
1121
|
function readRuleThinking(axes: ResolvedAxes): RuleThinking {
|
|
@@ -1108,6 +1136,7 @@ function readRuleThinking(axes: ResolvedAxes): RuleThinking {
|
|
|
1108
1136
|
if (typeof raw.supportsDisplay === "boolean") out.supportsDisplay = raw.supportsDisplay;
|
|
1109
1137
|
if (typeof raw.prefixBinding === "boolean") out.prefixBinding = raw.prefixBinding;
|
|
1110
1138
|
if (typeof raw.upgradeNeutral === "boolean") out.upgradeNeutral = raw.upgradeNeutral;
|
|
1139
|
+
if (typeof raw.unverified === "boolean") out.unverified = raw.unverified;
|
|
1111
1140
|
return out;
|
|
1112
1141
|
}
|
|
1113
1142
|
|
|
@@ -1155,13 +1184,13 @@ function resolveThinkingPolicy<TApi extends Api>(
|
|
|
1155
1184
|
// not re-expand it into an unadvertised ladder. Absent metadata is
|
|
1156
1185
|
// repaired only where KDL opts in with `thinking-upgrade-neutral`
|
|
1157
1186
|
// alongside a reviewed `thinking-efforts` ladder (the cascade upgrade for
|
|
1158
|
-
// stale source capability data)
|
|
1159
|
-
|
|
1187
|
+
// stale source capability data). On deployments whose discovery reports no
|
|
1188
|
+
// capabilities (`thinking-unverified`), an unclassified neutral spec — nothing
|
|
1189
|
+
// says whether it reasons — gets an unverified ladder the user may opt into.
|
|
1190
|
+
const unverified = !compatReasoning(spec, axes);
|
|
1160
1191
|
if (
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
"supportsReasoningEffort" in compat &&
|
|
1164
|
-
compat.supportsReasoningEffort === false
|
|
1192
|
+
unverified &&
|
|
1193
|
+
(explicitThinking !== undefined || facts.identity.class !== "unknown" || rule.unverified !== true)
|
|
1165
1194
|
) {
|
|
1166
1195
|
return undefined;
|
|
1167
1196
|
}
|
|
@@ -1180,9 +1209,14 @@ function resolveThinkingPolicy<TApi extends Api>(
|
|
|
1180
1209
|
if (config.efforts.length === 0) {
|
|
1181
1210
|
throw new Error(`Model ${spec.provider}/${spec.id} resolved to an empty thinking range`);
|
|
1182
1211
|
}
|
|
1183
|
-
if (rule.defaultLevel !== undefined) config.defaultLevel = rule.defaultLevel;
|
|
1184
1212
|
const effortMap = mergeEffortMap(spec, rule.effortMap, compat, config.efforts);
|
|
1185
1213
|
if (effortMap !== undefined) config.effortMap = effortMap;
|
|
1214
|
+
if (unverified) {
|
|
1215
|
+
// Off-by-default opt-in: no default level, mandatory effort, or wire off-switch.
|
|
1216
|
+
config.unverified = true;
|
|
1217
|
+
return config;
|
|
1218
|
+
}
|
|
1219
|
+
if (rule.defaultLevel !== undefined) config.defaultLevel = rule.defaultLevel;
|
|
1186
1220
|
if (rule.effortBudgets !== undefined) config.effortBudgets = rule.effortBudgets;
|
|
1187
1221
|
const supportsDisplay = rule.supportsDisplay ?? defaultSupportsDisplay(spec, facts);
|
|
1188
1222
|
if (supportsDisplay) config.supportsDisplay = true;
|
|
@@ -1279,7 +1313,7 @@ function buildResolveTarget<TApi extends Api>(
|
|
|
1279
1313
|
provider: providerType,
|
|
1280
1314
|
api: spec.api,
|
|
1281
1315
|
class: identity.class,
|
|
1282
|
-
model: spec
|
|
1316
|
+
model: policyModelId(spec),
|
|
1283
1317
|
reasoning: Boolean(spec.reasoning),
|
|
1284
1318
|
};
|
|
1285
1319
|
if (identity.family !== undefined) target.family = identity.family;
|
|
@@ -1315,7 +1349,8 @@ export function resolveModelPolicy<TApi extends Api>(
|
|
|
1315
1349
|
spec: ModelSpec<TApi>,
|
|
1316
1350
|
route?: ResolveRoute,
|
|
1317
1351
|
): ResolvedModelPolicy<TApi>;
|
|
1318
|
-
export function resolveModelPolicy(
|
|
1352
|
+
export function resolveModelPolicy(input: ModelSpec<Api>, route?: ResolveRoute): ResolvedModelPolicy<Api> {
|
|
1353
|
+
const spec = withoutUnverifiedThinking(input);
|
|
1319
1354
|
const identity = resolveIdentity(spec);
|
|
1320
1355
|
const facts = new IdentityFacts(identity);
|
|
1321
1356
|
const target = buildResolveTarget(spec, identity);
|
|
@@ -1338,6 +1373,8 @@ export function resolveModelPolicy(spec: ModelSpec<Api>, route?: ResolveRoute):
|
|
|
1338
1373
|
compat = resolveAnthropicPolicy(spec, facts, axes);
|
|
1339
1374
|
} else if (specUsesApi(spec, "bedrock-converse-stream")) {
|
|
1340
1375
|
compat = resolveBedrockPolicy(spec, axes);
|
|
1376
|
+
} else if (specUsesApi(spec, "ollama-chat")) {
|
|
1377
|
+
compat = resolveOllamaPolicy(spec, axes);
|
|
1341
1378
|
} else if (specUsesApi(spec, "devin-agent")) {
|
|
1342
1379
|
compat = resolveDevinPolicy(spec, axes);
|
|
1343
1380
|
} else if (
|