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:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
Reference in New Issue
Block a user