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
9 changes: 6 additions & 3 deletions apps/admin/src/lib/components/CreateKeyDialog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,18 @@ describe('CreateKeyDialog', () => {
// Tick a subset of lanes; the whitelist is the only lane cap (no max-lane field).
await fireEvent.click(screen.getByLabelText('economy'));
await fireEvent.click(screen.getByLabelText('balanced'));
await fireEvent.click(screen.getByLabelText('allow custom model'));
await fireEvent.input(screen.getByLabelText('Blocked models'), {
target: { value: 'gpt-4o\nanthropic/claude-sonnet-4-6' },
});
await fireEvent.click(screen.getByLabelText('allow client-requested Fast mode'));
// allow_custom_model defaults to false; leave that checkbox unchecked.
await fireEvent.click(screen.getByRole('button', { name: /create key/i }));

await waitFor(() => expect(createKey).toHaveBeenCalledTimes(1));
const input = createKey.mock.calls[0][0];
expect(input.allowed_lanes).toEqual(['economy', 'balanced']);
expect(input.blocked_models).toEqual(['gpt-4o', 'anthropic/claude-sonnet-4-6']);
expect(input.allow_custom_model).toBe(false);
expect(input.allow_custom_model).toBe(true);
expect(input.allow_fast_mode).toBe(true);
});

Expand All @@ -78,12 +78,15 @@ describe('CreateKeyDialog', () => {
expect(input.rate_limit_tpm).toBeUndefined();
});

it('collapses the optional cap sections by default, basics stay visible', () => {
it('collapses the optional cap sections by default, basics stay visible', async () => {
setup();
// Basics (lanes + passthrough) are immediately visible — not inside a section.
expect(screen.getByLabelText('economy')).toBeInTheDocument();
expect(screen.getByLabelText('allow custom model')).toBeInTheDocument();
expect(screen.getByLabelText('allow client-requested Fast mode')).toBeInTheDocument();
expect(screen.queryByLabelText('Blocked models')).not.toBeInTheDocument();
await fireEvent.click(screen.getByLabelText('allow custom model'));
expect(screen.getByLabelText('Blocked models')).toBeInTheDocument();
// The three optional groups render as <details> sections, all closed.
const sections = document.querySelectorAll('details');
expect(sections).toHaveLength(3);
Expand Down
34 changes: 18 additions & 16 deletions apps/admin/src/lib/components/KeyCapsForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -219,22 +219,24 @@
>
</div>

<label class="flex flex-col gap-1 text-sm">
<span class="field-label">{$t('Blocked models')}</span>
<textarea
rows="3"
aria-label={$t('Blocked models')}
placeholder={$t('One model id per line')}
class="input min-h-20 resize-y"
value={form.blockedModels.join('\n')}
oninput={(e) => updateBlockedModels(e.currentTarget.value)}
></textarea>
<span class="field-help"
>{$t(
'Blocks exact model ids for this key across direct model requests and all lane/fallback routes.',
)}</span
>
</label>
{#if form.allowCustomModel}
<label class="flex flex-col gap-1 text-sm">
<span class="field-label">{$t('Blocked models')}</span>
<textarea
rows="3"
aria-label={$t('Blocked models')}
placeholder={$t('One model id per line')}
class="input min-h-20 resize-y"
value={form.blockedModels.join('\n')}
oninput={(e) => updateBlockedModels(e.currentTarget.value)}
></textarea>
<span class="field-help"
>{$t(
'Blocks exact model ids for this key across direct model requests and all lane/fallback routes.',
)}</span
>
</label>
{/if}

<div class="flex flex-col gap-1">
<label class="checkbox-field">
Expand Down
7 changes: 2 additions & 5 deletions apps/admin/src/lib/i18n/mcp-locales.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import zhHant from '../../locales/zh-hant.json';
// ConnectMcpDialog (Memory page) ships localized prose around literal copy-paste
// snippets. These are static `$t('…')` calls so the extractor sees them — but a
// missing translation still silently falls back to English (the exact regression
// the Memory nav subtitle hit). Guard every translatable string. 'curl' is a proper
// noun (command name), identical across locales by design, so it is NOT asserted to
// differ from English.
// the Memory nav subtitle hit). Guard every translatable string.
const mcpKeys = [
'Connect via MCP',
"Give an AI agent persistent memory. Helm's MCP server exposes the facts and reflections on this page as tools (memory_add, memory_search, memory_list, …), scoped to the account behind your API key.",
Expand All @@ -21,8 +19,7 @@ const mcpKeys = [
'Codex and other stdio-only clients reach the HTTP server through the mcp-remote bridge in ~/.codex/config.toml.',
'Check connectivity and auth with a raw JSON-RPC call. It lists the memory tools the server exposes.',
'JSON config',
// ChatGPT OAuth walkthrough prose ('ChatGPT' itself is a proper noun, identical
// across locales by design, so — like 'curl' — it is not asserted to differ).
// ChatGPT OAuth walkthrough prose.
"ChatGPT connects over OAuth, not a bearer key — you won't paste your key into ChatGPT. Instead, ChatGPT opens a Helm login page where you paste it to authorize.",
'In ChatGPT, open Settings → Connectors and add a custom connector / MCP server (you may need to turn on developer mode).',
'Server URL: paste the URL above (it must end in /mcp).',
Expand Down
43 changes: 43 additions & 0 deletions apps/admin/src/lib/i18n/request-detail-locales.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';

import en from '../../locales/en.json';
import ja from '../../locales/ja.json';
import ko from '../../locales/ko.json';
import zhHans from '../../locales/zh-hans.json';
import zhHant from '../../locales/zh-hant.json';

const requestDetailPayloadKeys = [
'Load conversation',
'Load request body',
'Load response body',
'Load the captured request and response only when you need the transcript.',
'Load the full response body only when you need to inspect it.',
'Load the raw request body only when you need to inspect it.',
'Load this only when you need to compare the client body with the provider body.',
'Load upstream request',
'Loading',
'Payload capture is available for this call. Large bodies are loaded on demand.',
'Payload capture is available for this call. Large bodies are loaded only when you open a section.',
'Payload was not available.',
'The forwarded upstream body matched the client request body.',
] as const;

const translatedLocales = {
'zh-hans': zhHans,
'zh-hant': zhHant,
ja,
ko,
} as const;

describe('request detail payload locale coverage', () => {
it.each(Object.entries(translatedLocales))(
'%s translates lazy payload strings instead of falling back to English',
(_locale, dict: Record<string, string>) => {
for (const key of requestDetailPayloadKeys) {
expect(dict).toHaveProperty(key);
expect(dict[key]).toBeTruthy();
expect(dict[key]).not.toBe((en as Record<string, string>)[key]);
}
},
);
});
38 changes: 27 additions & 11 deletions apps/admin/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"{count} pending": "{count} pending",
"{count} running": "{count} running",
"{count} stale": "{count} stale",
"{count} system prompt hidden — show": "{count} system prompt hidden — show",
"{count} threads": "{count} threads",
"{count} turns": "{count} turns",
"{count}d": "{count}d",
Expand Down Expand Up @@ -86,6 +87,7 @@
"archived": "archived",
"Archived": "Archived",
"Archives": "Archives",
"Arguments": "Arguments",
"Assembled": "Assembled",
"Assistant": "Assistant",
"Authentication failed": "Authentication failed",
Expand All @@ -106,6 +108,8 @@
"Back to requests": "Back to requests",
"Background mode blocked across protocols": "Background mode blocked across protocols",
"Balanced": "Balanced",
"Blocked models": "Blocked models",
"Blocks exact model ids for this key across direct model requests and all lane/fallback routes.": "Blocks exact model ids for this key across direct model requests and all lane/fallback routes.",
"Budget": "Budget",
"By Key": "By Key",
"By Scope": "By Scope",
Expand Down Expand Up @@ -145,6 +149,7 @@
"Cleanup settings are saved with the “Save settings” button below.": "Cleanup settings are saved with the “Save settings” button below.",
"Clear": "Clear",
"Clear proxy": "Clear proxy",
"click to expand": "click to expand",
"Client disconnected": "Client disconnected",
"Client keys and their lane limits": "Client keys and their lane limits",
"Client registration: choose a predefined / custom OAuth client. Enter any Client ID (e.g. helm-mcp), leave the client secret empty, and set the token endpoint auth method to none.": "Client registration: choose a predefined / custom OAuth client. Enter any Client ID (e.g. helm-mcp), leave the client secret empty, and set the token endpoint auth method to none.",
Expand Down Expand Up @@ -259,6 +264,7 @@
"Edit limits": "Edit limits",
"Edit reflection": "Edit reflection",
"Edit the request below, then send it again. This runs a fresh call and records a new trace.": "Edit the request below, then send it again. This runs a fresh call and records a new trace.",
"empty": "empty",
"Empty match = catch-all. It matches every request and, by first-match order, swallows any rule below it — keep it last.": "Empty match = catch-all. It matches every request and, by first-match order, swallows any rule below it — keep it last.",
"Enable automatic cleanup": "Enable automatic cleanup",
"Enable Layer-2 eval": "Enable Layer-2 eval",
Expand Down Expand Up @@ -348,6 +354,7 @@
"Finishing…": "Finishing…",
"first match wins — lower number = higher priority": "first match wins — lower number = higher priority",
"first matching": "first matching",
"First reply": "First reply",
"Fit to window": "Fit to window",
"Fixed lower bound on how many requests may wait.": "Fixed lower bound on how many requests may wait.",
"Follow account priority first; rotate only within the same priority.": "Follow account priority first; rotate only within the same priority.",
Expand Down Expand Up @@ -445,8 +452,18 @@
"Leave the OAuth endpoints, Registration URL, and OIDC fields blank — Helm advertises them automatically via discovery.": "Leave the OAuth endpoints, Registration URL, and OIDC fields blank — Helm advertises them automatically via discovery.",
"Lets this client bypass lanes and target a specific model by name. Leave off to keep every request routed through lanes.": "Lets this client bypass lanes and target a specific model by name. Leave off to keep every request routed through lanes.",
"Lets this client request the provider's fast serving tier. If off, client Fast requests are downgraded unless the serving account forces Fast mode.": "Lets this client request the provider's fast serving tier. If off, client Fast requests are downgraded unless the serving account forces Fast mode.",
"lines": "lines",
"LLM Gateway": "LLM Gateway",
"Load conversation": "Load conversation",
"Load more": "Load more",
"Load request body": "Load request body",
"Load response body": "Load response body",
"Load the captured request and response only when you need the transcript.": "Load the captured request and response only when you need the transcript.",
"Load the full response body only when you need to inspect it.": "Load the full response body only when you need to inspect it.",
"Load the raw request body only when you need to inspect it.": "Load the raw request body only when you need to inspect it.",
"Load this only when you need to compare the client body with the provider body.": "Load this only when you need to compare the client body with the provider body.",
"Load upstream request": "Load upstream request",
"Loading": "Loading",
"Loading models…": "Loading models…",
"Loading proxy…": "Loading proxy…",
"Loading schedule…": "Loading schedule…",
Expand Down Expand Up @@ -520,6 +537,7 @@
"No requests recorded yet.": "No requests recorded yet.",
"No requests recorded yet. Point a client at the gateway to see routing activity here.": "No requests recorded yet. Point a client at the gateway to see routing activity here.",
"No reset credits available": "No reset credits available",
"no result": "no result",
"No routable models for this account.": "No routable models for this account.",
"No rule matched, so it defaulted to the balanced lane.": "No rule matched, so it defaulted to the balanced lane.",
"No streaming support": "No streaming support",
Expand Down Expand Up @@ -550,6 +568,7 @@
"on": "on",
"On failure": "On failure",
"Once clients send traffic through the gateway, every routing decision shows up here.": "Once clients send traffic through the gateway, every routing decision shows up here.",
"One model id per line": "One model id per line",
"Online": "Online",
"Open in new tab": "Open in new tab",
"Open navigation": "Open navigation",
Expand All @@ -570,6 +589,9 @@
"parked": "parked",
"Password (optional)": "Password (optional)",
"Paste the authorization code": "Paste the authorization code",
"Payload capture is available for this call. Large bodies are loaded on demand.": "Payload capture is available for this call. Large bodies are loaded on demand.",
"Payload capture is available for this call. Large bodies are loaded only when you open a section.": "Payload capture is available for this call. Large bodies are loaded only when you open a section.",
"Payload was not available.": "Payload was not available.",
"Per-key rate limits. Leave blank to use the system default. 0 means unlimited for that dimension.": "Per-key rate limits. Leave blank to use the system default. 0 means unlimited for that dimension.",
"Performance": "Performance",
"Point the Gemini CLI at Helm with these environment variables. The base URL is the bare origin — the CLI appends /v1beta/models/{model}:generateContent itself.": "Point the Gemini CLI at Helm with these environment variables. The base URL is the bare origin — the CLI appends /v1beta/models/{model}:generateContent itself.",
Expand Down Expand Up @@ -746,6 +768,8 @@
"Settings": "Settings",
"Show all {count} dimensions": "Show all {count} dimensions",
"Show all {count} turns": "Show all {count} turns",
"Show less": "Show less",
"Show more": "Show more",
"Show reasoning": "Show reasoning",
"Show remaining {count} items": "Show remaining {count} items",
"Show system": "Show system",
Expand Down Expand Up @@ -793,6 +817,7 @@
"The error class, if the request failed.": "The error class, if the request failed.",
"The exact request sent to the provider — after memory injection and protocol translation. This is what the model actually received.": "The exact request sent to the provider — after memory injection and protocol translation. This is what the model actually received.",
"The fallback limit for any key without its own per-key value. 0 means unlimited.": "The fallback limit for any key without its own per-key value. 0 means unlimited.",
"The forwarded upstream body matched the client request body.": "The forwarded upstream body matched the client request body.",
"The latest requests and how the gateway picked a lane for each one.": "The latest requests and how the gateway picked a lane for each one.",
"The Layer-2 model and its limits. Read-only — configured in": "The Layer-2 model and its limits. Read-only — configured in",
"The model that actually handled the request.": "The model that actually handled the request.",
Expand Down Expand Up @@ -885,6 +910,7 @@
"View": "View",
"View all": "View all",
"View all requests for this key": "View all requests for this key",
"view details": "view details",
"View full key": "View full key",
"View image": "View image",
"View source": "View source",
Expand Down Expand Up @@ -922,15 +948,5 @@
"Yesterday: {value}": "Yesterday: {value}",
"Your new API key": "Your new API key",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Show more": "Show more",
"Show less": "Show less",
"First reply": "First reply",
"Arguments": "Arguments",
"no result": "no result",
"lines": "lines",
"click to expand": "click to expand",
"view details": "view details",
"empty": "empty",
"{count} system prompt hidden — show": "{count} system prompt hidden — show"
"Zoom out": "Zoom out"
}
Loading
Loading