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: 2 additions & 1 deletion config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.",
"_rebaseline_2026_06_21_4475_target_format_badge": "PR #4475 (adivekar-utexas) review: src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts 955->974 (+19). Extracted the pure targetFormatBadgeI18nKey (the 6 targetFormat value->i18n-key mapping) out of the CustomModelsSection.tsx badge so it is unit-testable outside the .tsx (Rule #18 gap — the PR had no UI test). The .tsx now calls the helper instead of an inline if-chain. This leaf is the strangler-fig home for pure provider-page helpers (#3501), so receiving the extraction is on-purpose. Covered by tests/unit/provider-target-format-badge-4475.test.ts.",
"_rebaseline_2026_06_21_4427_low_noise_catalog": "PR #4427 (Rahulsharma0810) own growth: src/app/api/v1/models/catalog.ts 1478->1486 (+8). The opt-in MODELS_CATALOG_PREFIX_MODE (dual default | alias | canonical, with ?prefix= per-request override) gates the dual alias+canonical model emission at the three /v1/models push sites (static, synced, custom) behind includeAlias/includeCanonical, suppressing the duplicate cross-prefix entries (net +3 from the feature). On review, 4 incidental explanatory comments removed by the PR were restored (synced-models resolve, skip-static, try-block intent, strip-modelIdPrefix; +5) since their code is unchanged — useful docs on a non-trivial catalog function. Default `dual` keeps byte-identical output; request-side alias resolution unchanged. Structural shrink of this route tracked in #3789. Covered by tests/unit/models-catalog-low-noise-flag.test.ts.",
"_rebaseline_2026_06_21_qg9_chatcore_service_tier": "QG v2 Fase 9 T5 (#3501) — chatCore.ts 5137->5110 (shrink -27). The two inline Codex service-tier resolvers (resolveEffectiveServiceTier / resolveReportedServiceTier, ~36 LOC) were extracted byte-identically into the new pure leaf open-sse/handlers/chatCore/serviceTier.ts; the handler now keeps a `let effectiveServiceTier` + two thin binding closures that pass provider/credentials?.providerSpecificData to the extracted functions, so every call site stays unchanged. The orphaned getCodexRequestDefaults/normalizeCodexServiceTier/CodexServiceTier imports moved to the leaf. Ratcheted the frozen value down to lock the freed budget. Covered by tests/unit/chatcore-service-tier.test.ts.",
"_rebaseline_2026_06_20_reviewprs_mine_r2_filesize": "Reconciliacao file-size pos-lote /review-prs 'apenas minhas' r2: dois frozen cresceram cumulativamente sem bump (cada PR media OK na sua base, mas o crescimento empilhou acima do frozen no tip de merge; o fast-path do release nao roda check:file-size, so release->main). (1) src/shared/constants/pricing.ts 1620->1623 (+3 = linhas de pricing Claude Code (cc) do #4440, sobre o 1620 que o #4447 ja setara para gpt-4.1-mini/nano + o3/o4-mini). (2) open-sse/executors/base.ts 1399->1407 (+8 = handling granular de reasoning_effort para Claude no Copilot do #4443). Ambos dados/wiring coesos nos chokepoints existentes; nao extraiveis. Cobertos por tests/unit (claude-code pricing / base-executor-sanitize-effort + github-claude-reasoning-effort-granular).",
Expand Down Expand Up @@ -147,7 +148,7 @@
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 954,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderModels.ts": 155,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderSettings.ts": 264,
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 955,
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 974,
"src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx": 906,
"src/app/(dashboard)/dashboard/providers/page.tsx": 1927,
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1198,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
effectivePreserveForProtocol,
effectiveUpstreamHeadersForProtocol,
formatProviderModelsErrorResponse,
targetFormatBadgeI18nKey,
type CompatModelRow,
type CompatByProtocolMap,
} from "../providerPageHelpers";
Expand All @@ -38,6 +39,16 @@ export interface CustomModelsSectionProps {
onModelsChanged?: () => void;
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Map a targetFormat value to its display label, used for the model row badge. */
function targetFormatLabel(value: string, t: (key: string) => string): string {
const key = targetFormatBadgeI18nKey(value);
return key ? t(key) : value;
}

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
Expand All @@ -64,6 +75,11 @@ export default function CustomModelsSection({
const [editingModelId, setEditingModelId] = useState<string | null>(null);
const [editingApiFormat, setEditingApiFormat] = useState("chat-completions");
const [editingEndpoints, setEditingEndpoints] = useState<string[]>(["chat"]);
// #2905: per-model upstream wire-format override (empty string = no override,
// use provider default). Round-trips through the targetFormat field on the
// custom model record.
const [editingTargetFormat, setEditingTargetFormat] = useState("");
const [newTargetFormat, setNewTargetFormat] = useState("");
const [savingModelId, setSavingModelId] = useState<string | null>(null);
const [togglingModelId, setTogglingModelId] = useState<string | null>(null);

Expand Down Expand Up @@ -102,13 +118,15 @@ export default function CustomModelsSection({
modelName: newModelName.trim() || undefined,
apiFormat: newApiFormat,
supportedEndpoints: newEndpoints,
...(newTargetFormat ? { targetFormat: newTargetFormat } : {}),
}),
});
if (res.ok) {
setNewModelId("");
setNewModelName("");
setNewApiFormat("chat-completions");
setNewEndpoints(["chat"]);
setNewTargetFormat("");
await fetchCustomModels();
onModelsChanged?.();
}
Expand Down Expand Up @@ -164,12 +182,14 @@ export default function CustomModelsSection({
? model.supportedEndpoints
: ["chat"]
);
setEditingTargetFormat(model.targetFormat || "");
};

const cancelEdit = () => {
setEditingModelId(null);
setEditingApiFormat("chat-completions");
setEditingEndpoints(["chat"]);
setEditingTargetFormat("");
setSavingModelId(null);
};

Expand Down Expand Up @@ -225,6 +245,10 @@ export default function CustomModelsSection({
source: model?.source || "manual",
apiFormat: editingApiFormat,
supportedEndpoints: editingEndpoints,
// #2905: send targetFormat only when set; the API treats the field
// as optional. Sending an empty string would fail Zod's enum check,
// so we omit it entirely when the user picks "Default (auto)".
...(editingTargetFormat ? { targetFormat: editingTargetFormat } : {}),
}),
});

Expand Down Expand Up @@ -312,6 +336,26 @@ export default function CustomModelsSection({
<option value="images-generations">{t("imagesGenerations")}</option>
</select>
</div>
<div className="w-48">
<label htmlFor="custom-target-format" className="text-xs text-text-muted mb-1 block">
{t("targetFormatLabel")}
</label>
<select
id="custom-target-format"
value={newTargetFormat}
onChange={(e) => setNewTargetFormat(e.target.value)}
title={t("targetFormatHint")}
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
<option value="">{t("targetFormatAuto")}</option>
<option value="openai">{t("compatProtocolOpenAI")}</option>
<option value="openai-responses">{t("compatProtocolOpenAIResponses")}</option>
<option value="claude">{t("compatProtocolClaude")}</option>
<option value="gemini">{t("targetFormatGemini")}</option>
<option value="gemini-cli">{t("targetFormatGeminiCli")}</option>
<option value="antigravity">{t("targetFormatAntigravity")}</option>
</select>
</div>
<div className="flex-1">
<span className="text-xs text-text-muted mb-1 block">
{t("supportedEndpointsLabel")}
Expand Down Expand Up @@ -388,6 +432,14 @@ export default function CustomModelsSection({
{t("responses")}
</span>
)}
{model.targetFormat && (
<span
className="text-[10px] px-1.5 py-0.5 rounded-full bg-emerald-500/15 text-emerald-400 font-medium"
title={t("targetFormatHint")}
>
{`→ ${targetFormatLabel(model.targetFormat, t)}`}
</span>
)}
{model.supportedEndpoints?.includes("embeddings") && (
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-purple-500/15 text-purple-400 font-medium">
{`📐 ${t("supportedEndpointEmbeddings")}`}
Expand Down Expand Up @@ -445,13 +497,32 @@ export default function CustomModelsSection({
<option value="responses">{t("responsesApi")}</option>
<option value="embeddings">{t("embeddings")}</option>
<option value="rerank">Rerank</option>
<option value="audio-transcriptions">
{t("audioTranscriptions")}
</option>
<option value="audio-transcriptions">{t("audioTranscriptions")}</option>
<option value="audio-speech">{t("audioSpeech")}</option>
<option value="images-generations">{t("imagesGenerations")}</option>
</select>
</div>
<div className="w-[11rem] shrink-0 min-w-0">
<label className="text-xs text-text-muted mb-1 block">
{t("targetFormatLabel")}
</label>
<select
value={editingTargetFormat}
onChange={(e) => setEditingTargetFormat(e.target.value)}
title={t("targetFormatHint")}
className="w-full px-2.5 py-2 text-xs border border-border rounded-lg bg-background text-text-main focus:outline-none focus:border-primary"
>
<option value="">{t("targetFormatAuto")}</option>
<option value="openai">{t("compatProtocolOpenAI")}</option>
<option value="openai-responses">
{t("compatProtocolOpenAIResponses")}
</option>
<option value="claude">{t("compatProtocolClaude")}</option>
<option value="gemini">{t("targetFormatGemini")}</option>
<option value="gemini-cli">{t("targetFormatGeminiCli")}</option>
<option value="antigravity">{t("targetFormatAntigravity")}</option>
</select>
</div>
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-1 overflow-x-auto overflow-y-visible [scrollbar-width:thin]">
<span className="text-xs text-text-muted shrink-0">
{t("supportedEndpointsLabel")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,32 @@ export type CompatModelRow = {
isHidden?: boolean;
upstreamHeaders?: Record<string, string>;
compatByProtocol?: CompatByProtocolMap;
/** #2905: per-model upstream wire-format override. */ targetFormat?: string;
};

export type CompatModelMap = Map<string, CompatModelRow>;

export type HeaderDraftRow = { id: string; name: string; value: string };

// ---------------------------------------------------------------------------
// #2905 — per-model targetFormat badge label mapping (pure, so it can be unit-tested
// outside the .tsx). Returns the i18n key for a targetFormat value, or null when the
// value is unknown (the caller then renders the raw value verbatim).
// ---------------------------------------------------------------------------

const TARGET_FORMAT_BADGE_I18N_KEYS: Record<string, string> = {
openai: "compatProtocolOpenAI",
"openai-responses": "compatProtocolOpenAIResponses",
claude: "compatProtocolClaude",
gemini: "targetFormatGemini",
"gemini-cli": "targetFormatGeminiCli",
antigravity: "targetFormatAntigravity",
};

export function targetFormatBadgeI18nKey(value: string): string | null {
return TARGET_FORMAT_BADGE_I18N_KEYS[value] ?? null;
}

// ---------------------------------------------------------------------------
// Utility — message translation with fallback
// ---------------------------------------------------------------------------
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4263,6 +4263,12 @@
"compatProtocolOpenAI": "OpenAI Chat Completions",
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"targetFormatLabel": "Target Format",
"targetFormatHint": "Override the upstream wire format. Use 'Anthropic Messages' to route OpenAI-compatible providers (e.g. opencode-go) through the /v1/messages endpoint.",
"targetFormatAuto": "Default (auto)",
"targetFormatGemini": "Gemini",
"targetFormatGeminiCli": "Gemini CLI",
"targetFormatAntigravity": "Antigravity",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeaderName": "Header name",
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/provider-target-format-badge-4475.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Regression for #4475 (feat: targetFormat selector in custom-models form) — Rule #18.
*
* The form is a .tsx, so the testable surface is the pure label-mapping extracted into
* providerPageHelpers.ts. `targetFormatBadgeI18nKey` drives the model-row badge and must
* map exactly the 6 supported targetFormat values to their i18n keys, returning null for
* unknown values (the badge then renders the raw value). Pins the mapping so a future edit
* to the option list can't silently desync the badge from the form's <select> options.
*/
import test from "node:test";
import assert from "node:assert/strict";

import { targetFormatBadgeI18nKey } from "../../src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts";

test("maps each supported targetFormat value to its i18n key", () => {
assert.equal(targetFormatBadgeI18nKey("openai"), "compatProtocolOpenAI");
assert.equal(targetFormatBadgeI18nKey("openai-responses"), "compatProtocolOpenAIResponses");
assert.equal(targetFormatBadgeI18nKey("claude"), "compatProtocolClaude");
assert.equal(targetFormatBadgeI18nKey("gemini"), "targetFormatGemini");
assert.equal(targetFormatBadgeI18nKey("gemini-cli"), "targetFormatGeminiCli");
assert.equal(targetFormatBadgeI18nKey("antigravity"), "targetFormatAntigravity");
});

test("returns null for unknown / empty values (badge falls back to raw value)", () => {
assert.equal(targetFormatBadgeI18nKey(""), null);
assert.equal(targetFormatBadgeI18nKey("bogus"), null);
assert.equal(targetFormatBadgeI18nKey("auto"), null);
});
Loading