Skip to content
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@smithy/node-http-handler": "4.12.1",
"http-proxy-agent": "9.1.0",
"https-proxy-agent": "9.1.0",
"jose": "6.2.13",
"openai": "7.43.0",
"partial-json": "0.1.7",
"typebox": "1.4.3"
Expand Down
5 changes: 5 additions & 0 deletions packages/ai/src/api/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ export const stream: StreamFunction<"openai-responses", OpenAIResponsesOptions>
if (nextParams !== undefined) {
params = nextParams as ResponseCreateParamsStreaming;
}
if (model.provider === "openai-codex") {
params.model = model.id;
params.store = false;
params.stream = true;
}
const requestOptions = {
...(options?.signal ? { signal: options.signal } : {}),
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
Expand Down
7 changes: 7 additions & 0 deletions packages/ai/src/auth/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** A provider confirmed that this renewable session cannot be used again. */
export class OAuthSessionExpiredError extends Error {
constructor(message: string) {
super(message);
this.name = "OAuthSessionExpiredError";
}
}
8 changes: 8 additions & 0 deletions packages/ai/src/auth/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { ApiKeyAuth, OAuthAuth } from "./types.ts";

/** Keep retained identity tokens out of terminal output and diagnostic URLs. */
export function getOAuthDisplayUrl(value: string): string {
const url = new URL(value);
url.searchParams.delete("id_token_hint");
return url.toString();
}

/**
* Standard api-key auth: a stored credential key wins, otherwise the first
* set env var resolves. Includes a `login` that prompts for the key.
Expand Down Expand Up @@ -54,6 +61,7 @@ export function lazyOAuth(input: {
loginLabel: input.loginLabel,
login: async (interaction) => (await loaded()).login(interaction),
refresh: async (credential, signal) => (await loaded()).refresh(credential, signal),
logout: async (credential, signal) => (await loaded()).logout?.(credential, signal),
toAuth: async (credential) => (await loaded()).toAuth(credential),
};
}
73 changes: 73 additions & 0 deletions packages/ai/src/auth/oauth/chatgpt-registration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { randomUUID } from "node:crypto";
import { linkSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

export interface ChatGPTRegistration {
clientId: string;
subject?: string;
email?: string;
idToken?: string;
scopes?: string[];
}

/** Registration metadata survives logout; renewable tokens live in the credential store. */
export class ChatGPTRegistrationStore {
private readonly directory: string;

constructor() {
const configured = process.env.DIFFEX_CODING_AGENT_DIR;
const directory = configured?.startsWith("~/") ? join(homedir(), configured.slice(2)) : configured;
this.directory = join(directory || join(homedir(), ".diffex", "agent"), "chatgpt");
mkdirSync(this.directory, { recursive: true, mode: 0o700 });
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
}

hostId(): string {
const path = join(this.directory, "host");
const temporary = join(this.directory, `${randomUUID()}.tmp`);
// Publish a complete file exclusively when processes first sign in concurrently.
writeFileSync(temporary, `urn:uuid:${randomUUID()}`, { mode: 0o600, flag: "wx" });
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
try {
try {
linkSync(temporary, path);
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
}
const host = readFileSync(path, "utf8").trim();
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
if (!/^urn:uuid:[0-9a-f-]{36}$/.test(host)) throw new Error("Invalid saved ChatGPT host identity");
return host;
} finally {
unlinkSync(temporary);
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
}
}

list(): ChatGPTRegistration[] {
return readdirSync(this.directory)
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
.filter((name) => name.endsWith(".json"))
.map((name) => {
const value = JSON.parse(readFileSync(join(this.directory, name), "utf8")) as ChatGPTRegistration;
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
if (
!/^oaiapp_[\w-]+$/.test(value.clientId) ||
(value.subject !== undefined && (typeof value.subject !== "string" || !value.subject)) ||
(value.email !== undefined && typeof value.email !== "string") ||
(value.idToken !== undefined && typeof value.idToken !== "string") ||
(value.scopes !== undefined &&
(!Array.isArray(value.scopes) || !value.scopes.every((scope) => typeof scope === "string")))
) {
throw new Error("Invalid saved ChatGPT registration");
}
return value;
});
}

save(registration: ChatGPTRegistration): void {
if (!/^oaiapp_[\w-]+$/.test(registration.clientId)) throw new Error("Invalid issued ChatGPT client ID");
const temporary = join(this.directory, `${randomUUID()}.tmp`);
try {
writeFileSync(temporary, JSON.stringify(registration), { mode: 0o600, flag: "wx" });
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
renameSync(temporary, join(this.directory, `${registration.clientId}.json`));
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
} finally {
rmSync(temporary, { force: true });
Comment thread
Pepps233 marked this conversation as resolved.
Dismissed
}
}
}
Loading
Loading