fix(ai): aligned codex oauth originator

Used the shared Codex originator constant for browser OAuth URLs so login credentials match the headers sent by OMP Codex requests.

Added a regression test covering the browser-login URL originator.

Fixes #2696
This commit is contained in:
roboomp
2026-06-16 14:22:09 +02:00
committed by can1357
parent e24f789de4
commit 7d63c3589b
3 changed files with 48 additions and 13 deletions
+4
View File
@@ -17,6 +17,10 @@
- Fixed `validateToolArguments` silently accepting JSON-encoded array strings (e.g. `'["a","b"]'`) against `union(string, array<string>)` schemas — providers that double-serialize tool-call arguments (Z.AI / GLM) caused tools like `search` to receive the literal `["a","b"]` as a single path, producing zero matches (single element) or glob parse errors (multi-element). A new pre-validation pass parses JSON-array-shaped strings when the schema explicitly accepts both shapes. ([#1788](https://github.com/can1357/oh-my-pi/issues/1788))
- Fixed Anthropic thinking summaries that arrive wrapped in literal `<thinking>` tags so advisor/raw transcript dumps do not render nested thinking tags ([#2695](https://github.com/can1357/oh-my-pi/issues/2695)).
### Fixed
- Fixed Codex browser login issuing credentials for the `opencode` OAuth originator while OMP requests identify as `pi`, which could make the first authenticated Codex request return 401 ([#2696](https://github.com/can1357/oh-my-pi/issues/2696)).
## [16.0.0] - 2026-06-15
### Breaking Changes
+30 -13
View File
@@ -1,6 +1,8 @@
/**
* OpenAI Codex (ChatGPT OAuth) flow — browser and device-code flows.
*/
import { OPENAI_HEADER_VALUES } from "@oh-my-pi/pi-catalog/wire/codex";
import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
import { generatePKCE } from "./pkce";
import type { OAuthController, OAuthCredentials } from "./types";
@@ -60,6 +62,29 @@ interface PKCE {
verifier: string;
challenge: string;
}
/** Builds the Codex browser OAuth URL used by browser login; exported for auth regression tests. */
export function createOpenAICodexAuthorizationUrl(args: {
state: string;
redirectUri: string;
challenge: string;
originator?: string;
}): string {
const originator = args.originator?.trim() || OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
const searchParams = new URLSearchParams({
response_type: "code",
client_id: CLIENT_ID,
redirect_uri: args.redirectUri,
scope: SCOPE,
code_challenge: args.challenge,
code_challenge_method: "S256",
state: args.state,
id_token_add_organizations: "true",
codex_cli_simplified_flow: "true",
originator,
});
return `${AUTHORIZE_URL}?${searchParams.toString()}`;
}
class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
constructor(
@@ -79,20 +104,12 @@ class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
}
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
const searchParams = new URLSearchParams({
response_type: "code",
client_id: CLIENT_ID,
redirect_uri: redirectUri,
scope: SCOPE,
code_challenge: this.pkce.challenge,
code_challenge_method: "S256",
const url = createOpenAICodexAuthorizationUrl({
state,
id_token_add_organizations: "true",
codex_cli_simplified_flow: "true",
redirectUri,
challenge: this.pkce.challenge,
originator: this.originator,
});
const url = `${AUTHORIZE_URL}?${searchParams.toString()}`;
return { url, instructions: "A browser window should open. Complete login to finish." };
}
@@ -153,13 +170,13 @@ async function exchangeCodeForToken(code: string, verifier: string, redirectUri:
* Login with OpenAI Codex OAuth
*/
export type OpenAICodexLoginOptions = OAuthController & {
/** Optional originator value for OpenAI Codex OAuth. Default: "opencode". */
/** Optional originator value for OpenAI Codex OAuth. Default matches OMP Codex request headers. */
originator?: string;
};
export async function loginOpenAICodex(options: OpenAICodexLoginOptions): Promise<OAuthCredentials> {
const pkce = await generatePKCE();
const originator = options.originator?.trim() || "opencode";
const originator = options.originator?.trim() || OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
const flow = new OpenAICodexOAuthFlow(options, pkce, originator);
return flow.login();
+14
View File
@@ -1,13 +1,27 @@
import { describe, expect, it } from "bun:test";
import { createOpenAICodexAuthorizationUrl } from "@oh-my-pi/pi-ai/oauth/openai-codex";
import { type RequestBody, transformRequestBody } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
import { CodexApiError, parseCodexError } from "@oh-my-pi/pi-ai/providers/openai-codex/response-handler";
import { convertOpenAICodexResponsesTools } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
import type { Tool } from "@oh-my-pi/pi-ai/types";
import { OPENAI_HEADER_VALUES } from "@oh-my-pi/pi-catalog/wire/codex";
import { createCodexModel } from "./helpers";
const DEFAULT_PROMPT_PREFIX =
"You are an expert coding assistant. You help users with coding tasks by reading files, executing commands";
describe("openai-codex oauth", () => {
it("uses the same default originator for browser login and API requests", () => {
const authUrl = createOpenAICodexAuthorizationUrl({
state: "state",
redirectUri: "http://localhost:1455/auth/callback",
challenge: "challenge",
});
expect(new URL(authUrl).searchParams.get("originator")).toBe(OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
});
});
describe("openai-codex tool schemas", () => {
it("adds empty properties to no-argument object parameter schemas", () => {
const tools: Tool[] = [