mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
- H1: Add checkRateLimit to OAuth authorize/callback/disconnect + admin AI settings
- H2: Fix decrypt("") crash in provider.ts onTokenRefresh by using decrypt() directly
- H3: Truncate error messages in codex-oauth, codex-client, openai-client to prevent leaking upstream response bodies
- M5: Fix keyPreview to show last 4 chars of decrypted key instead of ciphertext prefix
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
78 lines
2.2 KiB
TypeScript
78 lines
2.2 KiB
TypeScript
import type { AIProvider, CompletionParams, CompletionResult } from "./types";
|
|
|
|
const CODEX_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
|
|
const CODEX_MODEL = "gpt-5.3-codex";
|
|
|
|
interface CodexClientConfig {
|
|
accessToken: string;
|
|
onTokenRefresh: () => Promise<string>;
|
|
}
|
|
|
|
export class CodexClient implements AIProvider {
|
|
readonly type = "codex" as const;
|
|
private accessToken: string;
|
|
private onTokenRefresh: () => Promise<string>;
|
|
|
|
constructor(config: CodexClientConfig) {
|
|
this.accessToken = config.accessToken;
|
|
this.onTokenRefresh = config.onTokenRefresh;
|
|
}
|
|
|
|
async generateCompletion(params: CompletionParams): Promise<CompletionResult> {
|
|
const firstAttempt = await this.doRequest(params);
|
|
|
|
if (firstAttempt.status === 401) {
|
|
this.accessToken = await this.onTokenRefresh();
|
|
const retryAttempt = await this.doRequest(params);
|
|
if (!retryAttempt.ok) {
|
|
throw new Error(`Codex request failed after token refresh (${retryAttempt.status})`);
|
|
}
|
|
return this.parseResponse(retryAttempt);
|
|
}
|
|
|
|
if (!firstAttempt.ok) {
|
|
throw new Error(`Codex request failed (${firstAttempt.status})`);
|
|
}
|
|
|
|
return this.parseResponse(firstAttempt);
|
|
}
|
|
|
|
private async doRequest(params: CompletionParams): Promise<Response> {
|
|
return fetch(CODEX_ENDPOINT, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${this.accessToken}`,
|
|
},
|
|
body: JSON.stringify({
|
|
model: CODEX_MODEL,
|
|
instructions: params.systemPrompt,
|
|
input: params.userPrompt,
|
|
stream: false,
|
|
}),
|
|
});
|
|
}
|
|
|
|
private async parseResponse(res: Response): Promise<CompletionResult> {
|
|
const json = await res.json();
|
|
|
|
const messageOutput = (json.output as Array<{ type: string; content?: Array<{ type: string; text?: string }> }>)?.find(
|
|
(o) => o.type === "message",
|
|
);
|
|
const textContent = messageOutput?.content?.find(
|
|
(c) => c.type === "output_text",
|
|
);
|
|
|
|
if (!textContent?.text) {
|
|
throw new Error("Codex returned empty content");
|
|
}
|
|
|
|
return {
|
|
content: textContent.text,
|
|
tokensUsed: json.usage?.total_tokens ?? null,
|
|
model: CODEX_MODEL,
|
|
providerType: "codex",
|
|
};
|
|
}
|
|
}
|