icebear0828 Claude Opus 4.6 commited on
Commit
8728276
·
1 Parent(s): 2def35e

chore: deduplicate model selector, auto-extract chromium version, harden update pipeline

Browse files

- Remove independent model dropdown from Anthropic SDK Setup; use shared Default Model from API Configuration
- Auto-extract Chromium version from Electron via electron-to-chromium in extract-fingerprint
- Add 5-min timeout kill timer for full-update child process to prevent permanent lock
- Add try/finally in model-fetcher initial timer to prevent refresh loop interruption
- Bump Codex Desktop to v26.228.1430 (build 760)
- Update poll interval from 30min to 3 days

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

.gitignore CHANGED
@@ -15,6 +15,7 @@ scripts/extract-fingerprint.ts
15
  scripts/full-update.ts
16
  scripts/apply-update.ts
17
  scripts/check-update.ts
 
18
  scripts/test-*.ts
19
  scripts/cron-update.sh
20
  config/extraction-patterns.yaml
 
15
  scripts/full-update.ts
16
  scripts/apply-update.ts
17
  scripts/check-update.ts
18
+ scripts/types.ts
19
  scripts/test-*.ts
20
  scripts/cron-update.sh
21
  config/extraction-patterns.yaml
CHANGELOG.md CHANGED
@@ -8,6 +8,7 @@
8
 
9
  ### Added
10
 
 
11
  - 动态模型列表:后台从 Codex 后端自动获取模型目录,与静态 YAML 合并(`src/models/model-store.ts`、`src/models/model-fetcher.ts`)
12
  - `/debug/models` 诊断端点,展示模型来源(static/backend)与刷新状态
13
  - 完整 Codex 模型目录:GPT-5.3/5.2/5.1 全系列 base/high/mid/low/max/mini 变体(23 个静态模型)
@@ -18,6 +19,7 @@
18
 
19
  ### Changed
20
 
 
21
  - 模型管理从纯静态 YAML 迁移至静态+动态混合架构(后端优先,YAML 兜底)
22
  - 默认模型改为 `gpt-5.2-codex`
23
  - Dashboard "Claude Code Quick Setup" 重命名为 "Anthropic SDK Setup"
@@ -25,6 +27,11 @@
25
 
26
  ### Fixed
27
 
 
 
 
 
 
28
  - 强化提示词注入防护:`SUPPRESS_PROMPT` 从弱 "ignore" 措辞改为声明式覆盖("NOT applicable"、"standard OpenAI API model"),解决 mini 模型仍泄露 Codex Desktop 身份的问题
29
  - 非流式请求错误处理:`collectTranslator` 抛出 generic Error 时返回 502 JSON 而非 500 HTML(`proxy-handler.ts`)
30
  - `desktop-context.md` 提取损坏修复:`extractPrompts()` 的 end marker 从 `` `; `` 改为 `` `[,;)] `` 正则,防止压缩 JS 代码注入 instructions 导致 tool_calls 失效(#13)
 
8
 
9
  ### Added
10
 
11
+ - 自动提取 Chromium 版本:`extract-fingerprint.ts` 从 `package.json` 读取 Electron 版本,通过 `electron-to-chromium` 映射为 Chromium 大版本,`apply-update.ts` 自动更新 `chromium_version` 和 TLS impersonate profile
12
  - 动态模型列表:后台从 Codex 后端自动获取模型目录,与静态 YAML 合并(`src/models/model-store.ts`、`src/models/model-fetcher.ts`)
13
  - `/debug/models` 诊断端点,展示模型来源(static/backend)与刷新状态
14
  - 完整 Codex 模型目录:GPT-5.3/5.2/5.1 全系列 base/high/mid/low/max/mini 变体(23 个静态模型)
 
19
 
20
  ### Changed
21
 
22
+ - Dashboard 模型选择器去重:移除 Anthropic SDK Setup 的独立模型下拉框,统一使用 API Configuration 的 Default Model
23
  - 模型管理从纯静态 YAML 迁移至静态+动态混合架构(后端优先,YAML 兜底)
24
  - 默认模型改为 `gpt-5.2-codex`
25
  - Dashboard "Claude Code Quick Setup" 重命名为 "Anthropic SDK Setup"
 
27
 
28
  ### Fixed
29
 
30
+ - `apply-update.ts` 模型比较不再误报删除:静态提取只含 2 个硬编码模型,与 YAML 的 24 个比较会产生 22 个假删除,现在只报新增
31
+ - `update-checker.ts` 子进程超时保护:`fork()` 添加 5 分钟 kill timer,防止挂起导致 `_updateInProgress` 永久锁定
32
+ - `model-fetcher.ts` 初始定时器添加 try/finally,防止异常中断刷新循环
33
+ - `apply-update.ts` 移除 `any` 类型(`mutateYaml` 回调参数)
34
+ - `ExtractedFingerprint` 接口统一:提取到 `scripts/types.ts` 共享,`extract-fingerprint.ts` 和 `apply-update.ts` 共用
35
  - 强化提示词注入防护:`SUPPRESS_PROMPT` 从弱 "ignore" 措辞改为声明式覆盖("NOT applicable"、"standard OpenAI API model"),解决 mini 模型仍泄露 Codex Desktop 身份的问题
36
  - 非流式请求错误处理:`collectTranslator` 抛出 generic Error 时返回 502 JSON 而非 500 HTML(`proxy-handler.ts`)
37
  - `desktop-context.md` 提取损坏修复:`extractPrompts()` 的 end marker 从 `` `; `` 改为 `` `[,;)] `` 正则,防止压缩 JS 代码注入 instructions 导致 tool_calls 失效(#13)
config/default.yaml CHANGED
@@ -3,8 +3,8 @@ api:
3
  timeout_seconds: 60
4
  client:
5
  originator: Codex Desktop
6
- app_version: 26.227.1448
7
- build_number: "747"
8
  platform: darwin
9
  arch: arm64
10
  chromium_version: "137"
 
3
  timeout_seconds: 60
4
  client:
5
  originator: Codex Desktop
6
+ app_version: 26.228.1430
7
+ build_number: "760"
8
  platform: darwin
9
  arch: arm64
10
  chromium_version: "137"
config/prompts/automation-response.md CHANGED
@@ -48,4 +48,4 @@ Response MUST end with a remark-directive block.
48
  - Waiting on user decision:
49
  - \`::inbox-item{title="Choose API shape for filters" summary="Two options drafted; pick A vs B"}\`
50
  - Status update with next step:
51
- - \`::inbox-item{title="PR comments addressed" summary="Ready for re-review; focus on auth edge case"}\`
 
48
  - Waiting on user decision:
49
  - \`::inbox-item{title="Choose API shape for filters" summary="Two options drafted; pick A vs B"}\`
50
  - Status update with next step:
51
+ - \`::inbox-item{title="PR comments addressed" summary="Ready for re-review; focus on auth edge case"}\
config/prompts/desktop-context.md CHANGED
@@ -34,7 +34,7 @@
34
  - Keep automation prompts self-sufficient because the user may have limited availability to answer questions. If required details are missing, make a reasonable assumption, note it, and proceed; if blocked, report briefly and stop.
35
  - When helpful, include clear output expectations (file path, format, sections) and gating rules (only if X, skip if exists) to reduce ambiguity.
36
  - Automations should always open an inbox item.
37
- - Archiving rule: only include `::archive-thread{}` when there is nothing actionable for the user.
38
  - Safe to archive: "no findings" checks (bug scans that found nothing, clean lint runs, monitoring checks with no incidents).
39
  - Do not archive: deliverables or follow-ups (briefs, reports, summaries, plans, recommendations).
40
  - If you do archive, include the archive directive after the inbox item.
@@ -67,4 +67,4 @@
67
 
68
  ### Archiving
69
  - If a user specifically asks you to end a thread/conversation, you can return the archive directive ::archive{...} to archive the thread/conversation.
70
- - Example: ::archive{reason="User requested to end conversation"}
 
34
  - Keep automation prompts self-sufficient because the user may have limited availability to answer questions. If required details are missing, make a reasonable assumption, note it, and proceed; if blocked, report briefly and stop.
35
  - When helpful, include clear output expectations (file path, format, sections) and gating rules (only if X, skip if exists) to reduce ambiguity.
36
  - Automations should always open an inbox item.
37
+ - Archiving rule: only include \`::archive-thread{}\` when there is nothing actionable for the user.
38
  - Safe to archive: "no findings" checks (bug scans that found nothing, clean lint runs, monitoring checks with no incidents).
39
  - Do not archive: deliverables or follow-ups (briefs, reports, summaries, plans, recommendations).
40
  - If you do archive, include the archive directive after the inbox item.
 
67
 
68
  ### Archiving
69
  - If a user specifically asks you to end a thread/conversation, you can return the archive directive ::archive{...} to archive the thread/conversation.
70
+ - Example: ::archive{reason="User requested to end conversation"}
package-lock.json CHANGED
@@ -19,6 +19,7 @@
19
  "@electron/asar": "^3.2.0",
20
  "@types/js-yaml": "^4.0.0",
21
  "@types/node": "^22.0.0",
 
22
  "js-beautify": "^1.15.0",
23
  "tsx": "^4.0.0",
24
  "typescript": "^5.5.0",
@@ -1310,6 +1311,13 @@
1310
  "url": "https://github.com/sponsors/isaacs"
1311
  }
1312
  },
 
 
 
 
 
 
 
1313
  "node_modules/emoji-regex": {
1314
  "version": "9.2.2",
1315
  "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
 
19
  "@electron/asar": "^3.2.0",
20
  "@types/js-yaml": "^4.0.0",
21
  "@types/node": "^22.0.0",
22
+ "electron-to-chromium": "^1.5.302",
23
  "js-beautify": "^1.15.0",
24
  "tsx": "^4.0.0",
25
  "typescript": "^5.5.0",
 
1311
  "url": "https://github.com/sponsors/isaacs"
1312
  }
1313
  },
1314
+ "node_modules/electron-to-chromium": {
1315
+ "version": "1.5.302",
1316
+ "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
1317
+ "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
1318
+ "dev": true,
1319
+ "license": "ISC"
1320
+ },
1321
  "node_modules/emoji-regex": {
1322
  "version": "9.2.2",
1323
  "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
package.json CHANGED
@@ -34,6 +34,7 @@
34
  "@electron/asar": "^3.2.0",
35
  "@types/js-yaml": "^4.0.0",
36
  "@types/node": "^22.0.0",
 
37
  "js-beautify": "^1.15.0",
38
  "tsx": "^4.0.0",
39
  "typescript": "^5.5.0",
 
34
  "@electron/asar": "^3.2.0",
35
  "@types/js-yaml": "^4.0.0",
36
  "@types/node": "^22.0.0",
37
+ "electron-to-chromium": "^1.5.302",
38
  "js-beautify": "^1.15.0",
39
  "tsx": "^4.0.0",
40
  "typescript": "^5.5.0",
scripts/extract-fingerprint.ts CHANGED
@@ -17,6 +17,7 @@ import { resolve, join } from "path";
17
  import { createHash } from "crypto";
18
  import { execSync } from "child_process";
19
  import yaml from "js-yaml";
 
20
 
21
  const ROOT = resolve(import.meta.dirname, "..");
22
  const OUTPUT_PATH = resolve(ROOT, "data/extracted-fingerprint.json");
@@ -36,29 +37,6 @@ interface ExtractionPatterns {
36
  }>;
37
  }
38
 
39
- interface ExtractedFingerprint {
40
- app_version: string;
41
- build_number: string;
42
- api_base_url: string | null;
43
- originator: string | null;
44
- models: string[];
45
- wham_endpoints: string[];
46
- user_agent_contains: string;
47
- sparkle_feed_url: string | null;
48
- prompts: {
49
- desktop_context_hash: string | null;
50
- desktop_context_path: string | null;
51
- title_generation_hash: string | null;
52
- title_generation_path: string | null;
53
- pr_generation_hash: string | null;
54
- pr_generation_path: string | null;
55
- automation_response_hash: string | null;
56
- automation_response_path: string | null;
57
- };
58
- extracted_at: string;
59
- source_path: string;
60
- }
61
-
62
  function sha256(content: string): string {
63
  return `sha256:${createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16)}`;
64
  }
@@ -123,6 +101,7 @@ function extractFromPackageJson(root: string): {
123
  version: string;
124
  buildNumber: string;
125
  sparkleFeedUrl: string | null;
 
126
  } {
127
  const pkgPath = join(root, "package.json");
128
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
@@ -131,6 +110,7 @@ function extractFromPackageJson(root: string): {
131
  version: pkg.version ?? "unknown",
132
  buildNumber: String(pkg.codexBuildNumber ?? "unknown"),
133
  sparkleFeedUrl: pkg.codexSparkleFeedUrl ?? null,
 
134
  };
135
  }
136
 
@@ -373,9 +353,32 @@ async function main() {
373
 
374
  // Step A: package.json
375
  console.log("[extract] Reading package.json...");
376
- const { version, buildNumber, sparkleFeedUrl } = extractFromPackageJson(asarRoot);
377
- console.log(` version: ${version}`);
378
- console.log(` build: ${buildNumber}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
  // Step B: main.js (or main-XXXXX.js chunk)
381
  console.log("[extract] Loading main.js...");
@@ -455,6 +458,8 @@ async function main() {
455
  const fingerprint: ExtractedFingerprint = {
456
  app_version: version,
457
  build_number: buildNumber,
 
 
458
  api_base_url: mainJsResults.apiBaseUrl,
459
  originator: mainJsResults.originator,
460
  models: mainJsResults.models,
 
17
  import { createHash } from "crypto";
18
  import { execSync } from "child_process";
19
  import yaml from "js-yaml";
20
+ import type { ExtractedFingerprint } from "./types.js";
21
 
22
  const ROOT = resolve(import.meta.dirname, "..");
23
  const OUTPUT_PATH = resolve(ROOT, "data/extracted-fingerprint.json");
 
37
  }>;
38
  }
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  function sha256(content: string): string {
41
  return `sha256:${createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16)}`;
42
  }
 
101
  version: string;
102
  buildNumber: string;
103
  sparkleFeedUrl: string | null;
104
+ electronVersion: string | null;
105
  } {
106
  const pkgPath = join(root, "package.json");
107
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
 
110
  version: pkg.version ?? "unknown",
111
  buildNumber: String(pkg.codexBuildNumber ?? "unknown"),
112
  sparkleFeedUrl: pkg.codexSparkleFeedUrl ?? null,
113
+ electronVersion: pkg.devDependencies?.electron ?? null,
114
  };
115
  }
116
 
 
353
 
354
  // Step A: package.json
355
  console.log("[extract] Reading package.json...");
356
+ const { version, buildNumber, sparkleFeedUrl, electronVersion } = extractFromPackageJson(asarRoot);
357
+ console.log(` version: ${version}`);
358
+ console.log(` build: ${buildNumber}`);
359
+ console.log(` electron: ${electronVersion ?? "not found"}`);
360
+
361
+ // Resolve Chromium version from Electron version
362
+ let chromiumVersion: string | null = null;
363
+ if (electronVersion) {
364
+ const electronMajor = parseInt(electronVersion.replace(/^[^0-9]*/, ""), 10);
365
+ if (!isNaN(electronMajor)) {
366
+ try {
367
+ const { versions } = await import("electron-to-chromium");
368
+ const versionMap = versions as Record<string, string>;
369
+ // versions keys use "major.minor" format (e.g. "40.0"), try both
370
+ const chromium = versionMap[`${electronMajor}.0`] ?? versionMap[electronMajor.toString()];
371
+ if (chromium) {
372
+ chromiumVersion = chromium;
373
+ console.log(` chromium: ${chromiumVersion} (from electron ${electronMajor})`);
374
+ } else {
375
+ console.warn(`[extract] No Chromium mapping for Electron ${electronMajor}`);
376
+ }
377
+ } catch {
378
+ console.warn("[extract] electron-to-chromium not available, skipping chromium resolution");
379
+ }
380
+ }
381
+ }
382
 
383
  // Step B: main.js (or main-XXXXX.js chunk)
384
  console.log("[extract] Loading main.js...");
 
458
  const fingerprint: ExtractedFingerprint = {
459
  app_version: version,
460
  build_number: buildNumber,
461
+ electron_version: electronVersion,
462
+ chromium_version: chromiumVersion,
463
  api_base_url: mainJsResults.apiBaseUrl,
464
  originator: mainJsResults.originator,
465
  models: mainJsResults.models,
src/models/model-fetcher.ts CHANGED
@@ -71,8 +71,11 @@ export function startModelRefresh(
71
 
72
  // Initial fetch after short delay
73
  _refreshTimer = setTimeout(async () => {
74
- await fetchModelsFromBackend(accountPool, cookieJar);
75
- scheduleNext(accountPool, cookieJar);
 
 
 
76
  }, INITIAL_DELAY_MS);
77
 
78
  console.log("[ModelFetcher] Scheduled initial model fetch in 5s");
 
71
 
72
  // Initial fetch after short delay
73
  _refreshTimer = setTimeout(async () => {
74
+ try {
75
+ await fetchModelsFromBackend(accountPool, cookieJar);
76
+ } finally {
77
+ scheduleNext(accountPool, cookieJar);
78
+ }
79
  }, INITIAL_DELAY_MS);
80
 
81
  console.log("[ModelFetcher] Scheduled initial model fetch in 5s");
src/update-checker.ts CHANGED
@@ -15,7 +15,7 @@ import { mutateYaml } from "./utils/yaml-mutate.js";
15
  const CONFIG_PATH = resolve(process.cwd(), "config/default.yaml");
16
  const STATE_PATH = resolve(process.cwd(), "data/update-state.json");
17
  const APPCAST_URL = "https://persistent.oaistatic.com/codex-app-prod/appcast.xml";
18
- const POLL_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
19
 
20
  export interface UpdateState {
21
  last_check: string;
@@ -77,6 +77,8 @@ function applyVersionUpdate(version: string, build: string): void {
77
  * Downloads new Codex.app, extracts fingerprint, and applies config updates.
78
  * Protected by a lock to prevent concurrent runs.
79
  */
 
 
80
  function triggerFullUpdate(): void {
81
  if (_updateInProgress) {
82
  console.log("[UpdateChecker] Full update already in progress, skipping");
@@ -95,6 +97,12 @@ function triggerFullUpdate(): void {
95
  },
96
  );
97
 
 
 
 
 
 
 
98
  let output = "";
99
  child.stdout?.on("data", (chunk: Buffer) => {
100
  output += chunk.toString();
@@ -104,6 +112,7 @@ function triggerFullUpdate(): void {
104
  });
105
 
106
  child.on("exit", (code) => {
 
107
  _updateInProgress = false;
108
  if (code === 0) {
109
  console.log("[UpdateChecker] Full update completed. Reloading config...");
@@ -125,6 +134,7 @@ function triggerFullUpdate(): void {
125
  });
126
 
127
  child.on("error", (err) => {
 
128
  _updateInProgress = false;
129
  console.error("[UpdateChecker] Failed to spawn full-update:", err.message);
130
  });
 
15
  const CONFIG_PATH = resolve(process.cwd(), "config/default.yaml");
16
  const STATE_PATH = resolve(process.cwd(), "data/update-state.json");
17
  const APPCAST_URL = "https://persistent.oaistatic.com/codex-app-prod/appcast.xml";
18
+ const POLL_INTERVAL_MS = 3 * 24 * 60 * 60 * 1000; // 3 days
19
 
20
  export interface UpdateState {
21
  last_check: string;
 
77
  * Downloads new Codex.app, extracts fingerprint, and applies config updates.
78
  * Protected by a lock to prevent concurrent runs.
79
  */
80
+ const UPDATE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
81
+
82
  function triggerFullUpdate(): void {
83
  if (_updateInProgress) {
84
  console.log("[UpdateChecker] Full update already in progress, skipping");
 
97
  },
98
  );
99
 
100
+ // Kill the child if it hangs beyond the timeout
101
+ const killTimer = setTimeout(() => {
102
+ console.warn("[UpdateChecker] Full update timed out, killing child");
103
+ child.kill("SIGTERM");
104
+ }, UPDATE_TIMEOUT_MS);
105
+
106
  let output = "";
107
  child.stdout?.on("data", (chunk: Buffer) => {
108
  output += chunk.toString();
 
112
  });
113
 
114
  child.on("exit", (code) => {
115
+ clearTimeout(killTimer);
116
  _updateInProgress = false;
117
  if (code === 0) {
118
  console.log("[UpdateChecker] Full update completed. Reloading config...");
 
134
  });
135
 
136
  child.on("error", (err) => {
137
+ clearTimeout(killTimer);
138
  _updateInProgress = false;
139
  console.error("[UpdateChecker] Failed to spawn full-update:", err.message);
140
  });
web/src/App.tsx CHANGED
@@ -39,7 +39,6 @@ function Dashboard() {
39
  />
40
  <AnthropicSetup
41
  apiKey={status.apiKey}
42
- models={status.models}
43
  selectedModel={status.selectedModel}
44
  />
45
  <CodeExamples
 
39
  />
40
  <AnthropicSetup
41
  apiKey={status.apiKey}
 
42
  selectedModel={status.selectedModel}
43
  />
44
  <CodeExamples
web/src/components/AnthropicSetup.tsx CHANGED
@@ -1,24 +1,22 @@
1
- import { useState, useMemo, useCallback } from "preact/hooks";
2
  import { useT } from "../i18n/context";
3
  import { CopyButton } from "./CopyButton";
4
 
5
  interface AnthropicSetupProps {
6
  apiKey: string;
7
- models: string[];
8
  selectedModel: string;
9
  }
10
 
11
- export function AnthropicSetup({ apiKey, models, selectedModel }: AnthropicSetupProps) {
12
  const t = useT();
13
- const [model, setModel] = useState(selectedModel);
14
 
15
  const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost:8080";
16
 
17
  const envLines = useMemo(() => ({
18
  ANTHROPIC_BASE_URL: origin,
19
  ANTHROPIC_API_KEY: apiKey,
20
- ANTHROPIC_MODEL: model,
21
- }), [origin, apiKey, model]);
22
 
23
  const allEnvText = useMemo(
24
  () => Object.entries(envLines).map(([k, v]) => `${k}=${v}`).join("\n"),
@@ -39,15 +37,6 @@ export function AnthropicSetup({ apiKey, models, selectedModel }: AnthropicSetup
39
  </svg>
40
  <h2 class="text-[0.95rem] font-bold">{t("anthropicSetup")}</h2>
41
  </div>
42
- <select
43
- class="px-3 py-1.5 text-xs font-mono rounded-lg bg-slate-100 dark:bg-bg-dark border border-gray-200 dark:border-border-dark text-slate-700 dark:text-text-main outline-none"
44
- value={model}
45
- onChange={(e) => setModel((e.target as HTMLSelectElement).value)}
46
- >
47
- {models.map((m) => (
48
- <option key={m} value={m}>{m}</option>
49
- ))}
50
- </select>
51
  </div>
52
 
53
  {/* Env vars */}
 
1
+ import { useMemo, useCallback } from "preact/hooks";
2
  import { useT } from "../i18n/context";
3
  import { CopyButton } from "./CopyButton";
4
 
5
  interface AnthropicSetupProps {
6
  apiKey: string;
 
7
  selectedModel: string;
8
  }
9
 
10
+ export function AnthropicSetup({ apiKey, selectedModel }: AnthropicSetupProps) {
11
  const t = useT();
 
12
 
13
  const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost:8080";
14
 
15
  const envLines = useMemo(() => ({
16
  ANTHROPIC_BASE_URL: origin,
17
  ANTHROPIC_API_KEY: apiKey,
18
+ ANTHROPIC_MODEL: selectedModel,
19
+ }), [origin, apiKey, selectedModel]);
20
 
21
  const allEnvText = useMemo(
22
  () => Object.entries(envLines).map(([k, v]) => `${k}=${v}`).join("\n"),
 
37
  </svg>
38
  <h2 class="text-[0.95rem] font-bold">{t("anthropicSetup")}</h2>
39
  </div>
 
 
 
 
 
 
 
 
 
40
  </div>
41
 
42
  {/* Env vars */}