fix(models): expose combo model token limits (#4189)

Integrated into release/v3.8.29. Thanks @megamen32! Reconciled on your branch: the branch had drifted behind release and would have reverted the centralized vision detection (#4072), the no-thinking gateway variants (#4145) and #4164's auto/* loop — those are kept, and your genuine improvement is applied on top: each auto/* /v1/models entry now carries advertised context/output limits + capabilities (createBuiltinAutoCombo, with a fallback to the minimal entry on resolve failure). Added a regression test (Rule #18) and bumped the catalog.ts file-size baseline 1440→1463 with justification.
This commit is contained in:
Demiurge The Single
2026-06-18 22:00:27 +03:00
committed by GitHub
parent 0abe9a3a40
commit 31f519b9b0
3 changed files with 78 additions and 8 deletions

View File

@@ -1,5 +1,6 @@
{
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.",
"_rebaseline_2026_06_18_4189_combo_token_limits": "PR #4189 (megamen32) own growth: catalog.ts 1440->1463 (+23 at the existing #4164 auto/* emission chokepoint). The bare auto/* /v1/models entries are enriched with the combo's advertised context/output limits (createBuiltinAutoCombo → advertisedContextLength/advertisedMaxOutputTokens computed from the candidate pool, 128000/8192 fallback) + baseline capabilities, with a try/catch that emits the minimal #4164 entry on resolve failure so the id is never dropped. OpenAI-compatible pickers (Hermes) need a context window before the first request. Cohesive emission at the single auto/* loop; not extractable.",
"_rebaseline_2026_06_18_4180_gemini_default_thinking": "PR #4180 own growth: openai-to-gemini.ts 844->864 (+20 = default includeThoughts for modern Gemini 2.5+ at the existing openaiToGeminiBase chokepoint — when the client set no thinkingConfig, inject one (includeThoughts:true + a capped budget) so the model's reasoning is marked thought:true and routed to reasoning_content instead of leaking into visible content, #4170). Cohesive translator branch gated to gemini-2.5+/3 (excludes gemini-1.x and non-thinking 2.0); not extractable. Reconciled here because #4180 merged without the baseline bump.",
"_rebaseline_2026_06_18_qg9_chatcore_split_prA": "QG v2 Fase 9 T5 C2-C3-C5: chatCore.ts 5445->5265 (wc -l 5264 + 1). Fase 2 of the chatCore split following #4159 (which created 10 leaf modules). Three more sibling leaves created under open-sse/handlers/chatCore/, all <cap, pure byte-identical moves (no runtime change): passthroughHelpers.ts (C2: shouldUseNativeCodexPassthrough/redactPassthroughThinkingSignatures/isClaudeCodeSemanticPassthroughRequest), responseHeaders.ts (C3: STREAMING_RESPONSE_HEADER_DENYLIST/buildStreamingResponseHeaders/materializeDeduplicatedExecutionResult/stripStaleForwardingHeaders), telemetryHelpers.ts (C5: forwardDashboardEventToLiveWs/maybeSyncClaudeExtraUsageState). chatCore.ts imports all 8 still-referenced symbols back from the leaves and re-exports the 5 previously-public ones (shouldUseNativeCodexPassthrough/redactPassthroughThinkingSignatures/isClaudeCodeSemanticPassthroughRequest/buildStreamingResponseHeaders/stripStaleForwardingHeaders) so existing test importers keep resolving. No barrel imports in the new leaves.",
"_rebaseline_2026_06_18_4176_free_models": "PR #4176 own growth: AddApiKeyModal.tsx 845->866 (+21) and EditConnectionModal.tsx 1174->1204 (+30) = the 'import only free models' connection option — a free-models Toggle gated by providerHasFreeModels() plus its form-state wiring (importFreeModelsOnly field + providerSpecificData persistence, explicit-false on edit so the PUT merge doesn't keep a stale true) added to both connection modals, mirroring the prior per-modal toggle bumps #3879 (redact-thinking) and #2997 (disable-cooling). Detection lives in the new shared src/shared/utils/freeModels.ts (112 LOC, <cap); the duplicated Toggle is ~6 lines and the per-modal state wiring is intrinsic, so the remaining growth is cohesive UI, not an extractable block.",
@@ -121,7 +122,7 @@
"src/app/api/providers/[id]/models/route.ts": 2531,
"src/app/api/providers/[id]/test/route.ts": 842,
"src/app/api/usage/analytics/route.ts": 941,
"src/app/api/v1/models/catalog.ts": 1440,
"src/app/api/v1/models/catalog.ts": 1463,
"src/lib/cloudflaredTunnel.ts": 934,
"src/lib/db/apiKeys.ts": 1661,
"src/lib/db/core.ts": 1820,

View File

@@ -20,7 +20,10 @@ import { getAllMusicModels } from "@omniroute/open-sse/config/musicRegistry";
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry";
import { CODEX_NATIVE_UNPREFIXED_MODELS } from "@omniroute/open-sse/services/model";
import { resolveNestedComboTargets } from "@omniroute/open-sse/services/combo";
import { AUTO_TEMPLATE_VARIANTS } from "@omniroute/open-sse/services/autoCombo/builtinCatalog";
import {
AUTO_TEMPLATE_VARIANTS,
createBuiltinAutoCombo,
} from "@omniroute/open-sse/services/autoCombo/builtinCatalog";
import { getAllSyncedAvailableModels, type SyncedAvailableModel } from "@/lib/db/models";
import { getCompatibleFallbackModels } from "@/lib/providers/managedAvailableModels";
import { getOpenRouterCatalog } from "@/lib/catalog/openrouterCatalog";
@@ -656,14 +659,16 @@ export async function getUnifiedModelsResponse(
const listedIds = new Set<string>();
// #4164: advertise the built-in zero-setup `auto/*` combos at the very top.
// They resolve on demand (createBuiltinAutoCombo) so they have no fixed
// targets/metadata to derive — emit a minimal combo-owned entry. The dashboard
// already shows these; clients that build their picker from /v1/models (e.g.
// Hermes) need them here too.
// #4189: enrich each with the combo's advertised context/output limits (computed
// by createBuiltinAutoCombo from its candidate pool) + baseline capabilities, so
// OpenAI-compatible clients that build their picker from /v1/models (e.g. Hermes)
// receive token metadata before the first request instead of a bare entry. If the
// combo cannot be materialized (e.g. no eligible connections yet) the minimal
// #4164 entry is emitted instead, so the id is never dropped.
for (const autoId of Object.keys(AUTO_TEMPLATE_VARIANTS)) {
if (listedIds.has(autoId)) continue;
listedIds.add(autoId);
models.push({
const baseAutoEntry = {
id: autoId,
object: "model",
created: timestamp,
@@ -671,7 +676,28 @@ export async function getUnifiedModelsResponse(
permission: [],
root: autoId,
parent: null,
});
};
try {
const suffix = autoId.replace(/^auto\/?/, "");
const virtualCombo = await createBuiltinAutoCombo(autoId, suffix);
const contextLength = virtualCombo.advertisedContextLength || 128000;
const maxOutputTokens = virtualCombo.advertisedMaxOutputTokens || 8192;
models.push({
...baseAutoEntry,
context_length: contextLength,
max_input_tokens: contextLength,
max_output_tokens: maxOutputTokens,
capabilities: {
tool_calling: true,
reasoning: true,
thinking: true,
temperature: true,
},
});
} catch (err) {
console.log(`[catalog] Could not materialize built-in auto model ${autoId}:`, err);
models.push(baseAutoEntry);
}
}
// Add combos first (they appear at the top) — only active ones

View File

@@ -83,3 +83,46 @@ test("#4164 no duplicate auto/* ids even if a persisted combo shadows one", asyn
const autoIds = body.data.map((m) => m.id).filter((id) => id.startsWith("auto/"));
assert.equal(autoIds.length, new Set(autoIds).size, "auto/* ids must be unique");
});
test("#4189 every auto/* entry exposes token limits + baseline capabilities", async () => {
// #4164 emitted a bare auto/* entry (no token metadata). #4189 enriches each with
// the combo's advertised context/output limits + baseline capabilities so
// OpenAI-compatible clients that build their picker from /v1/models get a context
// window before the first request. Without the fix these fields are absent.
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
assert.equal(response.status, 200);
const body = (await response.json()) as {
data: Array<{
id: string;
context_length?: number;
max_input_tokens?: number;
max_output_tokens?: number;
capabilities?: Record<string, boolean>;
}>;
};
const autoEntries = body.data.filter((m) => m.id.startsWith("auto/"));
assert.ok(autoEntries.length > 0, "sanity: at least one auto/* entry is listed");
for (const entry of autoEntries) {
assert.equal(
typeof entry.context_length,
"number",
`${entry.id} must expose a numeric context_length`
);
assert.ok((entry.context_length ?? 0) > 0, `${entry.id} context_length must be positive`);
assert.equal(typeof entry.max_input_tokens, "number", `${entry.id} must expose max_input_tokens`);
assert.equal(
typeof entry.max_output_tokens,
"number",
`${entry.id} must expose max_output_tokens`
);
assert.ok((entry.max_output_tokens ?? 0) > 0, `${entry.id} max_output_tokens must be positive`);
assert.ok(
entry.capabilities && typeof entry.capabilities === "object",
`${entry.id} must expose a capabilities map`
);
}
});