Skip to content

Commit 9ee1f76

Browse files
committed
Fix dynamic chat models and Codex spark handling
1 parent fb1f89e commit 9ee1f76

File tree

6 files changed

+216
-64
lines changed

6 files changed

+216
-64
lines changed

deno.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
{
22
"tasks": {
33
"dev": "deno serve --watch --env --allow-env --allow-net --allow-read --unstable-kv serve.ts",
4+
"dev:play": "deno task dev:local",
5+
"dev:local": "sh -c 'if [ -z \"${CODEX_AUTH_JSON_B64:-}\" ]; then if [ -f \"$HOME/.codex/auth.json\" ]; then export CODEX_AUTH_JSON_B64=\"$(base64 < \"$HOME/.codex/auth.json\" | tr -d \"\\n\")\"; echo \"Loaded CODEX_AUTH_JSON_B64 from ~/.codex/auth.json\"; else echo \"No CODEX_AUTH_JSON_B64 and no ~/.codex/auth.json found. /v1/models will return 503 until auth is configured.\"; fi; fi; if [ -f .env ]; then source .env; fi; deno serve --watch --allow-env --allow-net --allow-read --unstable-kv serve.ts'",
46
"ubq-ai": "deno run --env --allow-env --allow-net --allow-read scripts/ubq-ai.ts",
7+
"admin:models": "sh -c 'BASE_URL=${BASE_URL:-http://localhost:8000}; if [ -z \"${DENO_DEPLOY_TOKEN:-}\" ]; then echo \"DENO_DEPLOY_TOKEN is required for admin endpoints.\" >&2; exit 1; fi; curl -sS -H \"Authorization: Bearer ${DENO_DEPLOY_TOKEN}\" ${BASE_URL}/admin/codex/models'",
8+
"admin:models:spark": "sh -c 'BASE_URL=${BASE_URL:-http://localhost:8000}; if [ -z \"${DENO_DEPLOY_TOKEN:-}\" ]; then echo \"DENO_DEPLOY_TOKEN is required for admin endpoints.\" >&2; exit 1; fi; curl -sS -H \"Authorization: Bearer ${DENO_DEPLOY_TOKEN}\" ${BASE_URL}/admin/codex/models | jq -r \".data.models[]?.slug // empty | select(contains(\\\"-spark\\\"))\"'",
59
"upload:auth": "deno run --env --allow-env --allow-net --allow-read scripts/upload-codex-auth.ts",
610
"keys:create": "deno run --env --allow-env --allow-net --allow-read scripts/ubq-ai.ts admin keys create",
711
"keys:list": "deno run --env --allow-env --allow-net --allow-read scripts/ubq-ai.ts admin keys list",

src/codex.ts

Lines changed: 120 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { CodexAuthState, ResponseInputItem } from "./types.ts";
66
const CODEX_REFRESH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
77
const CODEX_REFRESH_TOKEN_URL = "https://auth.openai.com/oauth/token";
88
const CODEX_ORIGINATOR = "codex_cli_rs";
9-
const CODEX_USER_AGENT = "codex_cli_rs/0.99.0 (ai.ubq.fi)";
9+
const CODEX_USER_AGENT = "codex_cli_rs/0.100.0 (ai.ubq.fi)";
1010
const CODEX_CLIENT_VERSION = (() => {
1111
const match = CODEX_USER_AGENT.match(/codex_cli_rs\/([0-9]+(?:\.[0-9]+){1,2})/);
1212
return match ? match[1] : null;
@@ -111,44 +111,106 @@ export const cacheCodexAuth = (auth: CodexAuthState): void => {
111111
};
112112

113113
const loadAuthSeedFromEnv = (): CodexAuthState => {
114-
if (!config.codexAuthJsonB64) {
114+
if (config.isDeploy) {
115+
if (!config.codexAuthJsonB64) {
116+
throw new CodexError(
117+
"Codex auth missing: CODEX_AUTH_JSON_B64 unset and no KV entry.",
118+
"codex_auth_missing",
119+
503,
120+
);
121+
}
122+
}
123+
124+
if (config.codexAuthJsonB64) {
125+
let decoded: string;
126+
try {
127+
decoded = decodeBase64ToString(config.codexAuthJsonB64);
128+
} catch (error) {
129+
throw new CodexError(
130+
"Codex auth invalid: CODEX_AUTH_JSON_B64 is not valid base64.",
131+
"codex_auth_invalid",
132+
503,
133+
error,
134+
);
135+
}
136+
let parsed: unknown;
137+
try {
138+
parsed = JSON.parse(decoded) as unknown;
139+
} catch (error) {
140+
throw new CodexError(
141+
"Codex auth invalid: CODEX_AUTH_JSON_B64 is not valid JSON.",
142+
"codex_auth_invalid",
143+
503,
144+
error,
145+
);
146+
}
147+
const tokenData = parseCodexAuthFromAuthJson(parsed);
148+
if (!tokenData) {
149+
throw new CodexError(
150+
"Codex auth invalid: CODEX_AUTH_JSON_B64 does not look like a Codex auth.json.",
151+
"codex_auth_invalid",
152+
503,
153+
);
154+
}
155+
return { ...tokenData, updated_at_ms: Date.now() };
156+
}
157+
158+
if (config.isDeploy) {
115159
throw new CodexError(
116160
"Codex auth missing: CODEX_AUTH_JSON_B64 unset and no KV entry.",
117161
"codex_auth_missing",
118162
503,
119163
);
120164
}
121-
let decoded: string;
122-
try {
123-
decoded = decodeBase64ToString(config.codexAuthJsonB64);
124-
} catch (error) {
125-
throw new CodexError(
126-
"Codex auth invalid: CODEX_AUTH_JSON_B64 is not valid base64.",
127-
"codex_auth_invalid",
128-
503,
129-
error,
130-
);
165+
166+
return loadAuthSeedFromDisk();
167+
};
168+
169+
const loadAuthSeedFromDisk = (): CodexAuthState => {
170+
if (!config.isDeploy) {
171+
const home = (Deno as unknown as { homeDir?: () => string | null }).homeDir?.() ?? Deno.env.get("HOME");
172+
if (!home) {
173+
throw new CodexError("Could not resolve home directory for ~/.codex/auth.json.", "codex_auth_invalid", 503);
174+
}
175+
try {
176+
const raw = Deno.readTextFileSync(`${home}/.codex/auth.json`);
177+
const parsed = JSON.parse(raw) as unknown;
178+
const tokenData = parseCodexAuthFromAuthJson(parsed);
179+
if (!tokenData) {
180+
throw new CodexError(
181+
"Codex auth invalid: ~/.codex/auth.json does not look like a Codex auth.json.",
182+
"codex_auth_invalid",
183+
503,
184+
);
185+
}
186+
return { ...tokenData, updated_at_ms: Date.now() };
187+
} catch (error) {
188+
if (error instanceof CodexError) throw error;
189+
throw new CodexError(
190+
"Codex auth invalid: ~/.codex/auth.json is missing or unreadable.",
191+
"codex_auth_invalid",
192+
503,
193+
error,
194+
);
195+
}
131196
}
132-
let parsed: unknown;
133-
try {
134-
parsed = JSON.parse(decoded) as unknown;
135-
} catch (error) {
136-
throw new CodexError(
137-
"Codex auth invalid: CODEX_AUTH_JSON_B64 is not valid JSON.",
138-
"codex_auth_invalid",
139-
503,
140-
error,
141-
);
197+
throw new CodexError("Codex auth missing: CODEX_AUTH_JSON_B64 unset and no KV entry.", "codex_auth_missing", 503);
198+
};
199+
200+
const getConfiguredCodexAuthSeed = (): CodexAuthState | null => {
201+
if (!config.codexAuthJsonB64) {
202+
return loadAuthSeedFromEnv(); // throws when unavailable in deploy or returns fallback in local
142203
}
143-
const tokenData = parseCodexAuthFromAuthJson(parsed);
144-
if (!tokenData) {
145-
throw new CodexError(
146-
"Codex auth invalid: CODEX_AUTH_JSON_B64 does not look like a Codex auth.json.",
147-
"codex_auth_invalid",
148-
503,
149-
);
204+
try {
205+
return loadAuthSeedFromEnv();
206+
} catch {
207+
if (config.isDeploy) return null;
208+
try {
209+
return loadAuthSeedFromDisk();
210+
} catch {
211+
return null;
212+
}
150213
}
151-
return { ...tokenData, updated_at_ms: Date.now() };
152214
};
153215

154216
const getAuthEntry = async (): Promise<{
@@ -158,18 +220,44 @@ const getAuthEntry = async (): Promise<{
158220
}> => {
159221
const kv = await kvPromise;
160222
if (!kv) {
161-
const auth = cachedAuth ?? loadAuthSeedFromEnv();
223+
const auth = cachedAuth ?? getConfiguredCodexAuthSeed();
224+
if (!auth) {
225+
throw new CodexError(
226+
"Codex auth missing: CODEX_AUTH_JSON_B64 unset and no KV entry.",
227+
"codex_auth_missing",
228+
503,
229+
);
230+
}
162231
cachedAuth = auth;
163232
return { kv: null, entry: null, auth };
164233
}
165234

166235
const entry = await kv.get<CodexAuthState>(CODEX_KV_KEY);
167236
if (entry.value) {
237+
if (!config.isDeploy) {
238+
try {
239+
const localSeed = getConfiguredCodexAuthSeed();
240+
if (localSeed) {
241+
cachedAuth = localSeed;
242+
await kv.set(CODEX_KV_KEY, localSeed);
243+
return { kv, entry, auth: localSeed };
244+
}
245+
} catch {
246+
// Keep working from persisted KV credentials when local seed loading fails.
247+
}
248+
}
168249
cachedAuth = entry.value;
169250
return { kv, entry, auth: entry.value };
170251
}
171252

172-
const seed = loadAuthSeedFromEnv();
253+
const seed = getConfiguredCodexAuthSeed();
254+
if (!seed) {
255+
throw new CodexError(
256+
"Codex auth missing: CODEX_AUTH_JSON_B64 unset and no KV entry.",
257+
"codex_auth_missing",
258+
503,
259+
);
260+
}
173261
await kv.set(CODEX_KV_KEY, seed);
174262
cachedAuth = seed;
175263
return { kv, entry: null, auth: seed };

src/handler.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
requireAdminAuth,
2828
} from "./auth.ts";
2929
import { handleHealth, handleHealthAuth, handleHealthUpstream } from "./health.ts";
30-
import { corsHeaders, openaiError, withCors } from "./http.ts";
30+
import { corsHeaders, notFound, openaiError, withCors } from "./http.ts";
3131
import {
3232
getKernelUsageLimitSnapshot,
3333
incrementKernelOrgUsageLimit,
@@ -123,6 +123,10 @@ export default async function handler(req: Request): Promise<Response> {
123123
return withCors(await handleAdminCss());
124124
}
125125

126+
if (req.method === "GET" && path === "/favicon.ico") {
127+
return withCors(await handleFavicon());
128+
}
129+
126130
if (req.method === "GET" && path === "/app.js") {
127131
return withCors(await handleAppJs());
128132
}
@@ -268,7 +272,7 @@ export default async function handler(req: Request): Promise<Response> {
268272
}
269273

270274
if (!path.startsWith("/v1/")) {
271-
return withCors(openaiError(404, "Not found", "not_found"));
275+
return withCors(notFound());
272276
}
273277

274278
if (req.method === "GET" && path === "/v1/auth") {

src/http.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ export const openaiError = (
5656
});
5757
};
5858

59+
export const notFound = (): Response =>
60+
json(404, {
61+
error: {
62+
message: "Not found",
63+
type: "not_found",
64+
code: "not_found",
65+
},
66+
});
67+
5968
export const getBearerToken = (req: Request): string | null => {
6069
const value = req.headers.get("Authorization");
6170
if (!value) return null;

static/chat.html

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,7 @@
8989
<label data-field>
9090
<span data-label>Reasoning effort</span>
9191
<select id="reasoning-effort">
92-
<option value="">Default</option>
93-
<option value="none">none</option>
94-
<option value="minimal">minimal</option>
95-
<option value="low">low</option>
96-
<option value="medium">medium</option>
97-
<option value="high">high</option>
98-
<option value="xhigh">xhigh</option>
92+
<option value="">Loading models...</option>
9993
</select>
10094
</label>
10195
<label data-field>

0 commit comments

Comments
 (0)