Skip to content
Closed
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
18 changes: 15 additions & 3 deletions src-tauri/resources/codex_models/model-catalog.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"catalog_revision": 7,
"catalog_revision": 8,
"models": [
{
"slug": "gpt-6.1-sol",
Expand Down Expand Up @@ -170,10 +170,16 @@
"id": "priority",
"name": "Fast",
"description": "2x speed, increased usage"
},
{
"id": "ultrafast",
"name": "Ultrafast",
"description": "The fastest available responses for latency-sensitive work."
}
],
"additional_speed_tiers": [
"fast"
"fast",
"ultrafast"
],
"supports_reasoning_summary_parameter": true,
"supports_reasoning_summaries": true,
Expand Down Expand Up @@ -351,10 +357,16 @@
"id": "priority",
"name": "Fast",
"description": "2x speed, increased usage"
},
{
"id": "ultrafast",
"name": "Ultrafast",
"description": "The fastest available responses for latency-sensitive work."
}
],
"additional_speed_tiers": [
"fast"
"fast",
"ultrafast"
],
"supports_reasoning_summary_parameter": true,
"supports_reasoning_summaries": true,
Expand Down
54 changes: 51 additions & 3 deletions src-tauri/src/codex_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ mod tests {
let state = catalog_state().unwrap().read().unwrap();
let sources = &state.sources;
assert_eq!(sources.templates.len(), 10);
assert_eq!(sources.revision, 7);
assert_eq!(sources.revision, 8);
let embedded: Value = serde_json::from_str(MODEL_CATALOG_JSON).unwrap();
assert!(embedded.get("fallback_model").is_none());
let fallback: Value = serde_json::from_str(FALLBACK_MODEL_JSON).unwrap();
Expand Down Expand Up @@ -1237,6 +1237,48 @@ mod tests {
}
}

#[test]
fn embedded_ultrafast_tiers_survive_catalog_generation() {
let sources = parse_sources(MODEL_CATALOG_JSON).unwrap();
let runtime = runtime(serde_json::json!({"models": [
{"id": "gpt-6.1-sol"},
{"id": "gpt-6-astra"},
{"id": "gpt-6-luna"}
]}));
let catalog = prepare_catalog_with_sources(&runtime, &sources).unwrap();
let models = output_models(&catalog);

for slug in ["gpt-6.1-sol", "gpt-6-astra"] {
let model = models
.iter()
.find(|model| model["slug"] == slug)
.unwrap();
let template = &sources.templates[slug].value;
assert_eq!(model["service_tiers"], template["service_tiers"]);
assert_eq!(
model["additional_speed_tiers"],
serde_json::json!(["fast", "ultrafast"])
);
assert!(model["service_tiers"].as_array().unwrap().iter().any(|tier| {
tier["id"] == "ultrafast"
&& tier["name"] == "Ultrafast"
&& tier["description"].as_str().is_some_and(|text| !text.is_empty())
}));
assert_eq!(model["default_service_tier"], Value::Null);
}

let luna = models
.iter()
.find(|model| model["slug"] == "gpt-6-luna")
.unwrap();
assert_eq!(luna["additional_speed_tiers"], serde_json::json!(["fast"]));
assert!(luna["service_tiers"]
.as_array()
.unwrap()
.iter()
.all(|tier| tier["id"] != "ultrafast"));
}

#[test]
fn template_speed_tiers_are_preserved_when_fast_mode_is_enabled() {
let mut sources = test_sources();
Expand Down Expand Up @@ -1529,7 +1571,10 @@ mod tests {
assert_eq!(model["shell_type"], "shell_command");
assert_eq!(model["supports_reasoning_effort_updates"], true);
assert_eq!(model["default_service_tier"], Value::Null);
assert_eq!(model["additional_speed_tiers"], serde_json::json!(["fast"]));
assert_eq!(
model["additional_speed_tiers"],
serde_json::json!(["fast", "ultrafast"])
);
assert_eq!(model["service_tiers"], template["service_tiers"]);
assert_eq!(
model["base_instructions"],
Expand Down Expand Up @@ -1561,7 +1606,10 @@ mod tests {
["low", "medium", "high", "xhigh", "max", "ultra"]
);
assert_eq!(model["default_service_tier"], Value::Null);
assert_eq!(model["additional_speed_tiers"], serde_json::json!(["fast"]));
assert_eq!(
model["additional_speed_tiers"],
serde_json::json!(["fast", "ultrafast"])
);
assert_eq!(model["service_tiers"], template["service_tiers"]);
}

Expand Down
5 changes: 5 additions & 0 deletions src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,11 @@ export const jaOverrides = {
'usage.provider.unknownAccess': '接続方式の記録なし',
'usage.column.effort': '推論レベル',
'usage.speedMode.fast': 'fast',
'usage.speedMode.ultrafast': 'ultrafast',
'usage.speedMode.requested': 'リクエストのサービス階層',
'usage.speedMode.response': '応答のサービス階層',
'usage.speedMode.unrecorded': '記録なし',
'usage.speedMode.requestHint': 'バッジはリクエストの階層を示し、上流での適用や課金を確定するものではありません。',
'usage.column.request': 'リクエスト',
'usage.column.tokens': 'Token',
'usage.column.provider': 'プロバイダー',
Expand Down
5 changes: 5 additions & 0 deletions src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,11 @@ export const en: Record<MessageKey, string> = {
'usage.provider.unknownAccess': 'Access type unrecorded',
'usage.column.effort': 'Reasoning Effort',
'usage.speedMode.fast': 'fast',
'usage.speedMode.ultrafast': 'ultrafast',
'usage.speedMode.requested': 'Requested service tier',
'usage.speedMode.response': 'Response service tier',
'usage.speedMode.unrecorded': 'Not recorded',
'usage.speedMode.requestHint': 'The badge shows the requested tier, not confirmation of upstream scheduling or billing.',
'usage.column.request': 'Request',
'usage.column.tokens': 'Token',
'usage.column.provider': 'Provider',
Expand Down
5 changes: 5 additions & 0 deletions src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,11 @@ export const zhCN = {
'usage.provider.unknownAccess': '未记录接入方式',
'usage.column.effort': '推理强度',
'usage.speedMode.fast': 'fast',
'usage.speedMode.ultrafast': 'ultrafast',
'usage.speedMode.requested': '请求服务档位',
'usage.speedMode.response': '响应服务档位',
'usage.speedMode.unrecorded': '未记录',
'usage.speedMode.requestHint': '标记表示请求档位,不代表上游调度或计费已确认。',
'usage.column.request': '请求',
'usage.column.tokens': 'Token',
'usage.column.provider': '提供商',
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export type MessageVariables = Record<string, string | number>;

export const zhTW: Record<MessageKey, string> = {
...createTraditionalMessages(zhCN),
'usage.speedMode.requested': '請求服務檔位',
'usage.speedMode.response': '響應服務檔位',
'usage.speedMode.unrecorded': '未記錄',
'usage.speedMode.requestHint': '標記表示請求檔位,不代表上游調度或計費已確認。',
"app.theme.lightGroup": "淺色主題",
"app.theme.darkGroup": "深色主題",
'app.theme.triggerLabel': '外觀',
Expand Down
11 changes: 10 additions & 1 deletion src/pages/UsageEventsView.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
--usage-log-cache-read: #0f766e;
--usage-log-cache-write: #b45309;
--usage-log-response-model: #9a5808;
--usage-log-ultrafast: #7e22ce;
--usage-log-duration-good: #16a34a;
--usage-log-duration-warn: #d97706;
--usage-log-duration-bad: #dc2626;
Expand Down Expand Up @@ -133,7 +134,8 @@
white-space: nowrap;
}

.usage-request-log .usage-events-table .usage-model-badges > .usage-model-fast {
.usage-request-log .usage-events-table .usage-model-badges > .usage-model-fast,
.usage-request-log .usage-events-table .usage-model-badges > .usage-model-ultrafast {
display: inline-block;
width: fit-content;
padding: 1px 5px;
Expand All @@ -146,6 +148,12 @@
white-space: nowrap;
}

.usage-request-log .usage-events-table .usage-model-badges > .usage-model-ultrafast {
border-color: color-mix(in srgb, var(--usage-log-ultrafast) 36%, transparent);
background: color-mix(in srgb, var(--usage-log-ultrafast) 10%, transparent);
color: var(--usage-log-ultrafast);
}

.usage-request-log .usage-events-table .usage-td-model > .usage-response-model {
color: var(--usage-log-response-model);
}
Expand Down Expand Up @@ -460,6 +468,7 @@
--usage-log-cache-read: #5eead4;
--usage-log-cache-write: #fbbf24;
--usage-log-response-model: #f0bc73;
--usage-log-ultrafast: #d8b4fe;
--usage-log-duration-good: #4ade80;
--usage-log-duration-warn: #fbbf24;
--usage-log-duration-bad: #f87171;
Expand Down
11 changes: 9 additions & 2 deletions src/pages/UsageEventsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { formatDuration, formatUsageNumber } from '../services/usageNumber';
import { Dialog } from '../components/Dialog';
import { usageProviderDetails } from '../services/usageProvider';
import { usageModelDetails } from '../services/usageModel';
import { usageServiceTierDetails } from '../services/usageServiceTier';
import { Select } from '../components/Select';

const compactNumber = (value: number) => formatUsageNumber(value, getCurrentLocale());
Expand Down Expand Up @@ -326,21 +327,27 @@ function UsageEventCell({
case 'model': {
const model = usageModelDetails(record.model, record.alias, record.response_model);
const effort = record.reasoning_effort || 'auto';
const fast = ['priority', 'fast'].includes(record.service_tier?.trim().toLowerCase() ?? '');
const serviceTier = usageServiceTierDetails(record.service_tier, record.response_service_tier);
const serviceTierTitle = [
`${t('usage.speedMode.requested')}: ${serviceTier.requested || t('usage.speedMode.unrecorded')}`,
`${t('usage.speedMode.response')}: ${serviceTier.response || t('usage.speedMode.unrecorded')}`,
t('usage.speedMode.requestHint'),
].join('\n');
const modelTitle = [
`${t('usage.model.request')}: ${model.requested}`,
model.showResolved ? `${t('usage.model.upstream')}: ${model.resolved}` : '',
model.showResponseInTooltip ? `${t('usage.model.response')}: ${model.response}` : '',
model.mismatch ? t('usage.model.mismatch') : '',
model.showResponseInTooltip ? t('usage.model.responseHint') : '',
`${t('usage.column.effort')}: ${effort}`,
serviceTierTitle,
].filter(Boolean).join('\n');
return (
<td className="usage-stacked-cell usage-td-model align-left" title={modelTitle}>
<strong title={modelTitle}>{model.requested}</strong>
<span className="usage-model-badges">
<small className="usage-model-effort" title={effort}>{effort}</small>
{fast ? <small className="usage-model-fast" title={record.service_tier}>{t('usage.speedMode.fast')}</small> : null}
{serviceTier.badge ? <small className={`usage-model-${serviceTier.badge}`} title={serviceTierTitle}>{t(serviceTier.badge === 'ultrafast' ? 'usage.speedMode.ultrafast' : 'usage.speedMode.fast')}</small> : null}
</span>
{model.showResolved ? <small title={modelTitle}>{model.resolved}</small> : null}
{model.mismatch ? (
Expand Down
11 changes: 11 additions & 0 deletions src/services/usageServiceTier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** Keep the requested and reported tiers separate: a badge is not a billing confirmation. */
export function usageServiceTierDetails(requested?: string | null, response?: string | null) {
const requestedTier = requested?.trim() || '';
const responseTier = response?.trim() || '';
const normalized = requestedTier.toLowerCase();
const badge = normalized === 'ultrafast'
? 'ultrafast'
: normalized === 'priority' || normalized === 'fast' ? 'fast' : null;

return { requested: requestedTier, response: responseTier, badge };
}
3 changes: 2 additions & 1 deletion src/styles/usage-subpages.css
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ body.table-col-resizing .usage-records-page .usage-request-log .usage-events-tab
}

.usage-records-page .usage-request-log .usage-model-badges > .usage-model-effort,
.usage-records-page .usage-request-log .usage-model-badges > .usage-model-fast {
.usage-records-page .usage-request-log .usage-model-badges > .usage-model-fast,
.usage-records-page .usage-request-log .usage-model-badges > .usage-model-ultrafast {
margin: 0;
font-size: 11px;
font-weight: 650;
Expand Down
14 changes: 13 additions & 1 deletion tests/fixtures/usage-response-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const record = (id: string, model: string, alias: string, response_model?: strin
});

// Keep these cases in a stable order; the browser test uses it to inspect each model cell.
const records: UsageRecord[] = [
const baseRecords: UsageRecord[] = [
{ ...record('differing', 'gpt-6-astra', 'astra', 'gpt-5.6-luna'), service_tier: 'priority' },
{ ...record('matching', 'gpt-6-sol', 'same-alias', 'gpt-6-sol'), service_tier: 'fast' },
{ ...record('snapshot-prefix', 'openai/gpt-4o-latest', 'gpt4o', 'gpt-4o-2024-08-06'), service_tier: 'default' },
Expand All @@ -66,6 +66,18 @@ const records: UsageRecord[] = [
record('formula', 'formula-model', 'formula-alias', '=SUM(A1)'),
];

const tierRecords: UsageRecord[] = [
{ ...record('priority', 'gpt-6.1-sol', ''), service_tier: 'priority', response_service_tier: 'default' },
{ ...record('fast', 'gpt-6.1-sol', ''), service_tier: 'fast', response_service_tier: 'fast' },
{ ...record('ultrafast-default', 'gpt-6.1-sol', ''), reasoning_effort: 'xhigh', service_tier: 'ultrafast', response_service_tier: 'default' },
{ ...record('ultrafast-confirmed', 'gpt-6-astra', ''), service_tier: 'ultrafast', response_service_tier: 'ultrafast' },
{ ...record('normalized-ultrafast', 'gpt-6.1-sol', ''), service_tier: ' UltraFast ', response_service_tier: ' DEFAULT ' },
{ ...record('response-only', 'gpt-6.1-sol', ''), service_tier: 'default', response_service_tier: 'ultrafast' },
{ ...record('unknown-tier', 'gpt-6.1-sol', ''), service_tier: 'future-tier' },
record('legacy-tier', 'gpt-6.1-sol', ''),
];
const records = params.get('scenario') === 'service-tiers' ? tierRecords : baseRecords;

mockIPC(async () => null);

createRoot(document.getElementById('root')!).render(
Expand Down
12 changes: 12 additions & 0 deletions tests/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ import { jaOverrides } from '../src/i18n/ja';
import { pluginConfigText } from '../src/i18n/pluginConfig';

describe('i18n', () => {
it('localizes requested and reported service tiers without changing the technical badge', () => {
for (const locale of supportedLocales) {
expect(translate(locale, 'usage.speedMode.ultrafast')).toBe('ultrafast');
expect(translate(locale, 'usage.speedMode.requested')).not.toBe('');
expect(translate(locale, 'usage.speedMode.response')).not.toBe('');
expect(translate(locale, 'usage.speedMode.requestHint')).not.toBe('');
}
expect(translate('zh-TW', 'usage.speedMode.requested')).toBe('請求服務檔位');
expect(translate('zh-TW', 'usage.speedMode.response')).toBe('響應服務檔位');
expect(translate('zh-TW', 'usage.speedMode.unrecorded')).toBe('未記錄');
});

it('localizes the plugin discard confirmation in every language', () => {
for (const [locale, cancel, confirm] of [
['zh-CN', '继续编辑', '放弃修改'], ['zh-TW', '繼續編輯', '放棄修改'],
Expand Down
72 changes: 72 additions & 0 deletions tests/usage-service-tier-ui.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Run Vite first, then node tests/usage-service-tier-ui.cjs.
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');

const base = process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:1421';
const locales = {
'zh-CN': { requested: '请求服务档位', response: '响应服务档位', missing: '未记录' },
'zh-TW': { requested: '請求服務檔位', response: '響應服務檔位', missing: '未記錄' },
en: { requested: 'Requested service tier', response: 'Response service tier', missing: 'Not recorded' },
ja: { requested: 'リクエストのサービス階層', response: '応答のサービス階層', missing: '記録なし' },
};

(async () => {
const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--no-proxy-server'] });
try {
const page = await browser.newPage({ viewport: { width: 1480, height: 1000 } });
const errors = [];
page.on('pageerror', error => errors.push(String(error)));
await page.route('**/*', route => route.request().url().startsWith(`${base}/`) ? route.continue() : route.abort());
const open = async (locale, theme) => {
await page.goto(`${base}/tests/fixtures/usage-response-model.html?scenario=service-tiers&locale=${locale}&theme=${theme}`, { waitUntil: 'domcontentloaded' });
await page.locator('.usage-td-model').first().waitFor();
await page.evaluate(() => document.fonts.ready);
};

for (const [locale, labels] of Object.entries(locales)) {
await open(locale, 'light');
const cells = page.locator('.usage-td-model');
assert.equal(await cells.count(), 8);
assert.deepEqual(await page.locator('.usage-model-fast').allTextContents(), ['fast', 'fast']);
assert.deepEqual(await page.locator('.usage-model-ultrafast').allTextContents(), ['ultrafast', 'ultrafast', 'ultrafast']);
const title = await cells.nth(2).locator('.usage-model-ultrafast').getAttribute('title');
assert.ok(title.includes(`${labels.requested}: ultrafast`), `${locale}: requested tier is explicit`);
assert.ok(title.includes(`${labels.response}: default`), `${locale}: default response is retained`);
assert.ok((await cells.nth(3).getAttribute('title')).includes(`${labels.response}: ultrafast`));
assert.ok((await cells.nth(4).getAttribute('title')).includes(`${labels.requested}: UltraFast`));
for (const index of [5, 6, 7]) {
assert.equal(await cells.nth(index).locator('.usage-model-fast, .usage-model-ultrafast').count(), 0);
}
assert.ok((await cells.nth(5).getAttribute('title')).includes(`${labels.response}: ultrafast`));
assert.ok((await cells.nth(6).getAttribute('title')).includes(`${labels.requested}: future-tier`));
assert.ok((await cells.nth(6).getAttribute('title')).includes(`${labels.response}: ${labels.missing}`));
assert.ok((await cells.nth(7).getAttribute('title')).includes(`${labels.requested}: ${labels.missing}`));
}

for (const theme of ['light', 'dark']) {
await open('zh-CN', theme);
const cell = page.locator('.usage-td-model').nth(2);
const positions = await cell.locator('.usage-model-badges > small').evaluateAll(elements => elements.map(element => element.getBoundingClientRect().top));
assert.ok(Math.abs(positions[0] - positions[1]) < 2, `${theme}: Ultrafast stays beside xhigh`);
const colors = await page.locator('.usage-model-fast, .usage-model-ultrafast').evaluateAll(elements => elements.map(element => getComputedStyle(element).color));
assert.notEqual(colors[0], colors[2], `${theme}: Ultrafast is distinct from Fast`);
const clipped = await page.locator('.usage-model-ultrafast').evaluateAll(elements => elements.some(element => element.scrollWidth > element.clientWidth));
assert.equal(clipped, false, `${theme}: labels are not clipped`);
if (process.env.USAGE_SERVICE_TIER_SCREENSHOT_DIR) {
fs.mkdirSync(process.env.USAGE_SERVICE_TIER_SCREENSHOT_DIR, { recursive: true });
await page.screenshot({ path: path.join(process.env.USAGE_SERVICE_TIER_SCREENSHOT_DIR, `service-tiers-${theme}.png`), fullPage: true });
}
}

await page.setViewportSize({ width: 390, height: 850 });
await open('zh-CN', 'light');
const bounds = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth }));
assert.ok(bounds.scrollWidth <= bounds.width + 1, 'Narrow layout keeps scrolling within the table');
assert.deepEqual(errors, []);
console.log('PASS: service-tier badges, requested/reported distinction, four locales, both themes, and narrow layout.');
} finally {
await browser.close();
}
})().catch(error => { console.error(error); process.exitCode = 1; });
Loading