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
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
timeout-minutes: 15
env:
PANEL_RELEASE: ${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
PUBLIC_PANEL_BASE: ${{ vars.RELAY_PUBLIC_PANEL_BASE || 'https://relay.07230805.xyz' }}
PUBLIC_PANEL_BASE: ${{ secrets.RELAY_PUBLIC_PANEL_BASE || vars.RELAY_PUBLIC_PANEL_BASE }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/issue-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL || vars.OPENAI_BASE_URL || 'https://relay.07230805.xyz/v1' }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL || vars.OPENAI_BASE_URL }}
OPENAI_MODEL: ${{ secrets.OPENAI_MODEL || vars.OPENAI_MODEL || 'grok-4.5' }}
TRIAGE_ISSUE_NUMBER: ${{ github.event.inputs.issue_number || '' }}
# Auto-comment on newly opened issues (not on every edit) unless bot:quiet.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL || vars.OPENAI_BASE_URL || 'https://relay.07230805.xyz/v1' }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL || vars.OPENAI_BASE_URL }}
OPENAI_MODEL: ${{ secrets.OPENAI_MODEL || vars.OPENAI_MODEL || 'grok-4.5' }}
REVIEW_PR_NUMBER: ${{ github.event.inputs.pr_number || '' }}
steps:
Expand Down
134 changes: 134 additions & 0 deletions e2e/video-generation-page.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { expect, test, type Page } from "@playwright/test";

/**
* Renders the video models page against mocked management APIs.
*
* Not marked @critical: the page's behaviour is covered by unit tests, and this
* spec exists so the layout — highlighted curl block, endpoint switch, spec
* tables — is exercised in a real browser rather than only in jsdom.
*/

const VIDEO_MODEL = {
id: "grok-imagine-video-1.5",
provider: "xai",
display_name: "Grok Imagine Video",
description: "Grok Imagine text-to-video and image-to-video generation.",
supports_image_to_video: true,
max_duration_seconds: 15,
};

const seedAuth = async (page: Page) => {
await page.addInitScript(() => {
sessionStorage.setItem(
"code-proxy-admin-auth",
JSON.stringify({
apiBase: "http://127.0.0.1:8317",
managementKey: "cps_test",
rememberPassword: false,
expiresAt: Date.now() + 60_000,
}),
);
localStorage.setItem(
"cli-proxy-language",
JSON.stringify({ language: "zh-CN", state: { language: "zh-CN" } }),
);
});
};

const mockApis = async (page: Page) => {
const tenant = {
id: "t-system",
slug: "system",
name: "System Administration",
type: "system",
status: "active",
effective_status: "active",
expires_at: null,
description: "",
version: 1,
created_at: "",
updated_at: "",
};
await page.route("**/v0/auth/me", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
principal: {
kind: "user_session",
user: {
id: "u-admin",
tenant_id: "t-system",
username: "admin",
display_name: "Super Administrator",
status: "active",
must_change_password: false,
last_login_at: null,
role_ids: ["r-platform-admin"],
role_codes: ["platform_super_admin"],
version: 1,
created_at: "",
updated_at: "",
},
home_tenant: tenant,
effective_tenant: tenant,
roles: [],
permissions: ["system.config.read", "dashboard.read"],
platform_admin: true,
},
}),
}),
);
// Order matters: Playwright tries the most recently registered route first, so
// the catch-all has to be registered before the specific one it must not shadow.
await page.route("**/v0/management/**", (route) =>
route.fulfill({ status: 200, contentType: "application/json", body: "{}" }),
);
await page.route("**/v0/management/video-generation/models", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ models: [VIDEO_MODEL] }),
}),
);
};

test("renders the video call docs with a highlighted snippet", async ({ page }) => {
await seedAuth(page);
await mockApis(page);
await page.goto("/#/models/video-generation");

await expect(page.getByRole("heading", { name: "视频模型" })).toBeVisible();

const codeBlock = page.locator("[data-code-block]").first();
await expect(codeBlock).toContainText("curl http://127.0.0.1:8317/v1/videos/generations");
await expect(codeBlock).toContainText("/v1/videos/$REQUEST_ID");

// Highlighting must produce coloured token spans, not one flat text node.
const tokenColours = await codeBlock.evaluate((element) => {
const spans = [...element.querySelectorAll("span")];
return new Set(spans.map((span) => getComputedStyle(span).color)).size;
});
expect(tokenColours).toBeGreaterThan(2);

await page.getByRole("tab", { name: "图生视频" }).click();
await expect(page.locator("[data-code-block]").first()).toContainText('"image": { "url"');

await expect(page.getByText("请求参数")).toBeVisible();
await expect(page.getByText("返回结构")).toBeVisible();
});

test("enables the test panel from the served model catalog", async ({ page }) => {
await seedAuth(page);
await mockApis(page);
await page.goto("/#/models/video-generation");

// The button stays disabled until the catalog answers, which is what made the
// panel unreachable when the models call was shadowed by a catch-all mock.
const testButton = page.getByRole("button", { name: "测试生成" });
await expect(testButton).toBeEnabled();

await testButton.click();
await expect(page.getByText("测试视频生成")).toBeVisible();
await expect(page.getByText(VIDEO_MODEL.id, { exact: false }).first()).toBeVisible();
});
3 changes: 3 additions & 0 deletions features/api-key-restrictions/useApiKeyPermissionOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export function useApiKeyPermissionOptions() {
openCodeGoKeys,
clineKeys,
ollamaCloudKeys,
commandCodeKeys,
vertexKeys,
openaiProviders,
authFiles,
Expand All @@ -142,6 +143,7 @@ export function useApiKeyPermissionOptions() {
providersApi.getOpenCodeGoConfigs().catch(() => []),
providersApi.getClineConfigs().catch(() => []),
providersApi.getOllamaCloudConfigs().catch(() => []),
providersApi.getCommandCodeConfigs().catch(() => []),
providersApi.getVertexConfigs().catch(() => []),
providersApi.getOpenAIProviders().catch(() => []),
authFilesApi.list().catch(() => ({ files: [] })),
Expand Down Expand Up @@ -173,6 +175,7 @@ export function useApiKeyPermissionOptions() {
openCodeGoKeys.forEach((item) => push(item.name || "", "API", "opencode-go"));
clineKeys.forEach((item) => push(item.name || "", "API", "cline"));
ollamaCloudKeys.forEach((item) => push(item.name || "", "API", "ollama-cloud"));
commandCodeKeys.forEach((item) => push(item.name || "", "API", "commandcode"));
vertexKeys.forEach((item) => push(item.name || "", "API", "vertex"));
openaiProviders.forEach((item) => push(item.name || "", "API", "openai"));
(authFiles.files || []).forEach((file) => {
Expand Down
2 changes: 2 additions & 0 deletions features/model-availability/modelAvailability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,15 @@ const PROVIDER_CHANNELS = [
{ key: "cline", load: () => providersApi.getClineConfigs() },
{ key: "opencode-go", load: () => providersApi.getOpenCodeGoConfigs() },
{ key: "ollama-cloud", load: () => providersApi.getOllamaCloudConfigs() },
{ key: "commandcode", load: () => providersApi.getCommandCodeConfigs() },
{ key: "vertex", load: () => providersApi.getVertexConfigs() },
] as const;

const MODEL_ACCESS_PROVIDER_KEYS = new Set([
"cline",
"opencode-go",
"ollama-cloud",
"commandcode",
]);

const emptyAvailability = (
Expand Down
5 changes: 4 additions & 1 deletion features/model-tags/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,10 @@ export function ModelTag({

return (
<span
title={title ?? id}
// No implicit title: it repeated the visible model id as a second, native
// tooltip stacked on top of the managed one. Callers that truncate the tag
// wrap it in OverflowTooltip, which only opens when the text is cut off.
title={title}
className={cn(
"inline-flex max-w-full items-center border font-mono font-semibold leading-none",
sizeClasses.wrapper,
Expand Down
59 changes: 59 additions & 0 deletions features/request-log-viewer/RequestLogModelCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useTranslation } from "react-i18next";
import { isDistinctModelIdentity } from "@code-proxy/domain";
import { HoverTooltip, OverflowTooltip } from "@code-proxy/ui";
import { ModelTag } from "@features/model-tags";
import type { RequestLogsRow } from "./requestLogsShared";

function ModelHintDot({
label,
value,
toneClass,
}: {
label: string;
value: string;
toneClass: string;
}) {
return (
<HoverTooltip content={`${label}\n${value}`} placement="top">
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${toneClass}`} aria-label={label} />
</HoverTooltip>
);
}

/**
* Model column of the request log table.
*
* The hint dots only appear for a genuinely different model. An account alias that
* merely adds a routing prefix (`ollama/deepseek-v4-flash:0731` for upstream
* `deepseek-v4-flash:0731`) is the same model under two names, and announcing it as
* a "real model ID" was noise on every single row of an aliased provider.
*/
export function RequestLogModelCell({ row }: { row: RequestLogsRow }) {
const { t } = useTranslation();
if (!row.model) {
return <span className="text-xs text-slate-400 dark:text-white/30">--</span>;
}

const label = row.displayModel || row.model;
return (
<span className="inline-flex max-w-full items-center justify-center gap-1 align-middle">
<OverflowTooltip content={label} className="min-w-0">
<ModelTag id={label} size="sm" className="align-middle" />
</OverflowTooltip>
{isDistinctModelIdentity(row.model, row.upstreamModel) ? (
<ModelHintDot
label={t("request_logs.real_model_id")}
value={row.upstreamModel}
toneClass="bg-amber-500"
/>
) : null}
{isDistinctModelIdentity(row.model, row.visionFallbackModel) ? (
<ModelHintDot
label={t("request_logs.vision_fallback_model_id")}
value={row.visionFallbackModel}
toneClass="bg-sky-500"
/>
) : null}
</span>
);
}
35 changes: 2 additions & 33 deletions features/request-log-viewer/requestLogsShared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { SearchableCheckboxMultiSelect, Tabs, TabsList, TabsTrigger } from "@cod
import type { SearchableCheckboxMultiSelectOption } from "@code-proxy/ui";
import { HoverTooltip, OverflowTooltip } from "@code-proxy/ui";
import { PaginationBar } from "@code-proxy/ui";
import { ModelTag } from "@features/model-tags";
import { RequestLogModelCell } from "./RequestLogModelCell";

export type TimeRange = 1 | 7 | 14 | 30;
export type StatusFilterValue = "success" | "failed";
Expand Down Expand Up @@ -940,38 +940,7 @@ export function buildRequestLogsColumns(
width: "w-44",
headerClassName: CENTERED_REQUEST_LOG_HEADER_CLASS,
cellClassName: "text-center",
render: (row) =>
row.model ? (
<span className="inline-flex max-w-full items-center justify-center gap-1 align-middle">
<OverflowTooltip content={row.displayModel || row.model} className="min-w-0">
<ModelTag id={row.displayModel || row.model} size="sm" className="align-middle" />
</OverflowTooltip>
{row.upstreamModel && row.upstreamModel !== row.model ? (
<HoverTooltip
content={`${t("request_logs.real_model_id")}\n${row.upstreamModel}`}
placement="top"
>
<span
className="h-1.5 w-1.5 shrink-0 rounded-full bg-amber-500"
aria-label={t("request_logs.real_model_id")}
/>
</HoverTooltip>
) : null}
{row.visionFallbackModel && row.visionFallbackModel !== row.model ? (
<HoverTooltip
content={`${t("request_logs.vision_fallback_model_id")}\n${row.visionFallbackModel}`}
placement="top"
>
<span
className="h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500"
aria-label={t("request_logs.vision_fallback_model_id")}
/>
</HoverTooltip>
) : null}
</span>
) : (
<span className="text-xs text-slate-400 dark:text-white/30">--</span>
),
render: (row) => <RequestLogModelCell row={row} />,
},
);
return identityColumn === "none"
Expand Down
Loading
Loading