Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions providers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ billing:
per million tokens. Change `pricingRef` whenever rates or effective dates
change. Declare `longContext` when a model changes rates above an input-token
threshold; omit `pricing` when a model cannot be priced safely.
- `models.entries[].supportedReasoningEfforts` advertises the model's exact
provider-native OpenAI-compatible wire efforts. Values are unique and limited
to `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`.

## Edge Support Rules

Expand Down
7 changes: 7 additions & 0 deletions providers/_schema/service-provider.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,13 @@
"id": { "type": "string" },
"upstream": { "type": "string" },
"capabilities": { "type": "array", "items": { "type": "string" } },
"supportedReasoningEfforts": {
"type": "array",
"items": { "enum": ["none", "minimal", "low", "medium", "high", "xhigh", "max"] },
"minItems": 1,
"maxItems": 7,
"uniqueItems": true
},
"pricingRef": { "type": "string" },
"pricing": { "$ref": "#/$defs/modelPricing" }
}
Expand Down
1 change: 1 addition & 0 deletions providers/openai.provider.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ models:
- id: openai/gpt-5.6
upstream: gpt-5.6
capabilities: [llm.responses, llm.chat]
supportedReasoningEfforts: [none, low, medium, high, xhigh, max]
pricingRef: openai-gpt-5.6-standard-2026-07-09
pricing:
effectiveAt: "2026-07-09"
Expand Down
10 changes: 10 additions & 0 deletions scripts/compile-providers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function compileProviderSnapshot(manifests) {
provider: provider.id,
upstream: model.upstream,
capabilities: model.capabilities,
...(model.supportedReasoningEfforts ? { supportedReasoningEfforts: model.supportedReasoningEfforts } : {}),
pricing_ref: model.pricing_ref,
pricing: model.pricing,
};
Expand Down Expand Up @@ -106,6 +107,7 @@ function compileProvider(manifest, ids) {
id: model.id,
upstream: model.upstream,
capabilities: model.capabilities ?? [],
...(model.supportedReasoningEfforts ? { supportedReasoningEfforts: model.supportedReasoningEfforts } : {}),
pricing_ref: model.pricingRef ?? null,
pricing: model.pricing ? normalizePricing(model.pricing) : null,
}));
Expand Down Expand Up @@ -212,6 +214,14 @@ function validateManifest(manifest) {
for (const capability of manifest.capabilities) {
if (!manifest.endpoints[capability.endpoint]) throw new Error(`provider ${manifest.id} capability ${capability.id} references missing endpoint ${capability.endpoint}`);
}
const reasoningEfforts = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
for (const model of manifest.models?.entries ?? []) {
const efforts = model.supportedReasoningEfforts;
if (efforts === undefined) continue;
if (!Array.isArray(efforts) || efforts.length === 0 || efforts.length > reasoningEfforts.size) throw new Error(`provider ${manifest.id} model ${model.id} supportedReasoningEfforts must contain 1-${reasoningEfforts.size} entries`);
if (efforts.some((effort) => !reasoningEfforts.has(effort))) throw new Error(`provider ${manifest.id} model ${model.id} supportedReasoningEfforts contains an unsupported effort`);
if (new Set(efforts).size !== efforts.length) throw new Error(`provider ${manifest.id} model ${model.id} supportedReasoningEfforts must contain unique entries`);
}
for (const [id, endpoint] of Object.entries(manifest.endpoints)) {
if (!endpoint.path?.startsWith("/")) throw new Error(`provider ${manifest.id} endpoint ${id} path must start with /`);
for (const placeholder of endpoint.path.matchAll(/\$\{([^}]+)\}/g)) {
Expand Down
2 changes: 1 addition & 1 deletion worker/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export function catalogModels(provider: CompiledProvider, endpoints: string[], p
// Budgeted proxy keys fail closed at discovery instead of per-request pricing_required.
if (requiresPricing && model.pricing == null) return [];
const capabilities = executableCapabilities(provider, model.capabilities, endpoints);
return capabilities.length ? [{ id: model.id, upstream: model.upstream, capabilities, pricing_ref: model.pricing_ref, pricing: model.pricing }] : [];
return capabilities.length ? [{ id: model.id, upstream: model.upstream, capabilities, ...(model.supportedReasoningEfforts ? { supportedReasoningEfforts: model.supportedReasoningEfforts } : {}), pricing_ref: model.pricing_ref, pricing: model.pricing }] : [];
});
}

Expand Down
2 changes: 1 addition & 1 deletion worker/generated/provider-snapshot.json

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions worker/test/discovery.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ const { catalogModels } = await import("../discovery.ts");
const fireworks = providerById("fireworks");
assert.ok(fireworks);
const endpoints = fireworks.endpoints.map((endpoint) => endpoint.id);
const openai = providerById("openai");
assert.ok(openai);
const openaiEndpoints = openai.endpoints.map((endpoint) => endpoint.id);

test("catalog models preserve declared reasoning efforts without adding sibling metadata", () => {
const models = catalogModels(openai, openaiEndpoints, null);
const gpt56 = models.find((model) => model.id === "openai/gpt-5.6");
const gpt55 = models.find((model) => model.id === "openai/gpt-5.5");

assert.deepEqual(gpt56.supportedReasoningEfforts, ["none", "low", "medium", "high", "xhigh", "max"]);
assert.equal("supportedReasoningEfforts" in gpt55, false);
});

test("budgeted proxy-key catalogs omit unpriced models without fixed request pricing", () => {
const models = catalogModels(fireworks, endpoints, {
Expand Down
33 changes: 33 additions & 0 deletions worker/test/provider-compiler.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ test("TypeScript provider compiler is deterministic and preserves the catalog co
const gpt56 = openai.models.find((model) => model.id === "openai/gpt-5.6");
assert.equal(gpt56.upstream, "gpt-5.6");
assert.deepEqual(gpt56.capabilities, ["llm.responses", "llm.chat"]);
assert.deepEqual(gpt56.supportedReasoningEfforts, ["none", "low", "medium", "high", "xhigh", "max"]);
assert.deepEqual(compiled.model_index["openai/gpt-5.6"].supportedReasoningEfforts, gpt56.supportedReasoningEfforts);
assert.equal("supportedReasoningEfforts" in openai.models.find((model) => model.id === "openai/gpt-5.5"), false);
assert.deepEqual(gpt56.pricing, {
effectiveAt: "2026-07-09",
source: "https://developers.openai.com/api/docs/models/gpt-5.6-sol",
Expand Down Expand Up @@ -50,6 +53,17 @@ test("TypeScript provider compiler is deterministic and preserves the catalog co
assert.ok(compiled.capability_index["llm.chat"].length >= 10);
});

test("provider schema bounds reasoning efforts to canonical wire values", () => {
const schema = JSON.parse(readFileSync("providers/_schema/service-provider.schema.json", "utf8"));
assert.deepEqual(schema.$defs.model.properties.supportedReasoningEfforts, {
type: "array",
items: { enum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"] },
minItems: 1,
maxItems: 7,
uniqueItems: true,
});
});

test("compiled providers have unique ids, models, capabilities, and executable endpoint references", () => {
const snapshot = JSON.parse(readFileSync("worker/generated/provider-snapshot.json", "utf8"));
assert.equal(new Set(snapshot.providers.map((provider) => provider.id)).size, snapshot.providers.length);
Expand Down Expand Up @@ -85,3 +99,22 @@ test("quota header sources must retain their declared array shape", () => {
rmSync(directory, { recursive: true, force: true });
}
});

test("reasoning effort metadata rejects empty, duplicate, and unsupported values", () => {
const directory = mkdtempSync(join(tmpdir(), "clawrouter-provider-"));
const manifest = join(directory, "openai.provider.yaml");
const source = readFileSync("providers/openai.provider.yaml", "utf8");
const cases = [
["[]", /must contain 1-7 entries/],
["[none, low, low]", /must contain unique entries/],
["[none, ultra]", /contains an unsupported effort/],
];
try {
for (const [value, expected] of cases) {
writeFileSync(manifest, source.replace("[none, low, medium, high, xhigh, max]", value));
assert.throws(() => execFileSync(process.execPath, ["scripts/compile-providers.mjs", manifest], { encoding: "utf8", stdio: "pipe" }), expected);
}
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
5 changes: 3 additions & 2 deletions worker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export interface ProviderSnapshot {
version: string;
providers: CompiledProvider[];
capability_index: Record<string, Array<{ provider: string; endpoint: string; methods: string[] }>>;
model_index: Record<string, { provider: string; upstream: string; capabilities: string[]; pricing_ref: string | null; pricing: ModelPricing | null }>;
model_index: Record<string, { provider: string; upstream: string; capabilities: string[]; supportedReasoningEfforts?: ProviderReasoningEffort[]; pricing_ref: string | null; pricing: ModelPricing | null }>;
}

export interface CompiledProvider {
Expand Down Expand Up @@ -59,7 +59,8 @@ export interface AuthorizationConfig { authorizeUrl: string; tokenUrl: string; c
export interface RefreshConfig { tokenUrl: string; clientId: string | null; clientIdConfig: string | null; clientSecretConfig: string | null; extraParams: Record<string, string> }
export interface LongContextPricing { thresholdInputTokens: number; inputMicrosPerMillion: number; outputMicrosPerMillion: number; cachedInputMicrosPerMillion: number | null; cacheWriteInputMicrosPerMillion: number | null; cacheWrite5mInputMicrosPerMillion: number | null; cacheWrite1hInputMicrosPerMillion: number | null }
export interface ModelPricing { effectiveAt: string; source: string; inputMicrosPerMillion: number; outputMicrosPerMillion: number; cachedInputMicrosPerMillion: number | null; cacheWriteInputMicrosPerMillion: number | null; cacheWrite5mInputMicrosPerMillion: number | null; cacheWrite1hInputMicrosPerMillion: number | null; maxInputTokens: number; maxRequestInputTokens: number | null; defaultMaxOutputTokens: number; inputTokenOverhead: number; longContext: LongContextPricing | null }
export interface CompiledModel { id: string; upstream: string; capabilities: string[]; pricing_ref: string | null; pricing: ModelPricing | null }
export type ProviderReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
export interface CompiledModel { id: string; upstream: string; capabilities: string[]; supportedReasoningEfforts?: ProviderReasoningEffort[]; pricing_ref: string | null; pricing: ModelPricing | null }
export interface CompiledEndpoint { id: string; method: string; methods: string[]; path: string; native_proxy: boolean; auth: string | null; headers: Record<string, string>; request_headers: string[]; response_headers: string[]; query: Record<string, string>; path_params: string[]; path_param_styles: Record<string, string>; request_format: string; response_format: string; streaming: string | null; timeout_ms: number | null }

export interface Env {
Expand Down
Loading