mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:10:44 +00:00
fix: resolve Codex native auth by profile provider
This commit is contained in:
committed by
Peter Steinberger
parent
12d90a26f7
commit
8ea04f994a
@@ -7,10 +7,26 @@ import {
|
||||
readCodexAppServerBinding,
|
||||
resolveCodexAppServerBindingPath,
|
||||
writeCodexAppServerBinding,
|
||||
type CodexAppServerAuthProfileLookup,
|
||||
} from "./session-binding.js";
|
||||
|
||||
let tempDir: string;
|
||||
|
||||
const nativeAuthLookup: Pick<CodexAppServerAuthProfileLookup, "authProfileStore"> = {
|
||||
authProfileStore: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
work: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("codex app-server session binding", () => {
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-binding-"));
|
||||
@@ -46,21 +62,25 @@ describe("codex app-server session binding", () => {
|
||||
|
||||
it("does not persist public OpenAI as the provider for Codex-native auth bindings", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.json");
|
||||
await writeCodexAppServerBinding(sessionFile, {
|
||||
threadId: "thread-123",
|
||||
cwd: tempDir,
|
||||
authProfileId: "openai-codex:work",
|
||||
model: "gpt-5.4-mini",
|
||||
modelProvider: "openai",
|
||||
});
|
||||
await writeCodexAppServerBinding(
|
||||
sessionFile,
|
||||
{
|
||||
threadId: "thread-123",
|
||||
cwd: tempDir,
|
||||
authProfileId: "work",
|
||||
model: "gpt-5.4-mini",
|
||||
modelProvider: "openai",
|
||||
},
|
||||
nativeAuthLookup,
|
||||
);
|
||||
|
||||
const raw = await fs.readFile(resolveCodexAppServerBindingPath(sessionFile), "utf8");
|
||||
const binding = await readCodexAppServerBinding(sessionFile);
|
||||
const binding = await readCodexAppServerBinding(sessionFile, nativeAuthLookup);
|
||||
|
||||
expect(raw).not.toContain('"modelProvider": "openai"');
|
||||
expect(binding).toMatchObject({
|
||||
threadId: "thread-123",
|
||||
authProfileId: "openai-codex:work",
|
||||
authProfileId: "work",
|
||||
model: "gpt-5.4-mini",
|
||||
});
|
||||
expect(binding?.modelProvider).toBeUndefined();
|
||||
@@ -75,7 +95,7 @@ describe("codex app-server session binding", () => {
|
||||
threadId: "thread-123",
|
||||
sessionFile,
|
||||
cwd: tempDir,
|
||||
authProfileId: "openai-codex:work",
|
||||
authProfileId: "work",
|
||||
model: "gpt-5.4-mini",
|
||||
modelProvider: "openai",
|
||||
createdAt: "2026-05-03T00:00:00.000Z",
|
||||
@@ -83,12 +103,53 @@ describe("codex app-server session binding", () => {
|
||||
})}\n`,
|
||||
);
|
||||
|
||||
const binding = await readCodexAppServerBinding(sessionFile);
|
||||
const binding = await readCodexAppServerBinding(sessionFile, nativeAuthLookup);
|
||||
|
||||
expect(binding?.authProfileId).toBe("openai-codex:work");
|
||||
expect(binding?.authProfileId).toBe("work");
|
||||
expect(binding?.modelProvider).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not infer native Codex auth from the profile id prefix", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.json");
|
||||
await writeCodexAppServerBinding(
|
||||
sessionFile,
|
||||
{
|
||||
threadId: "thread-123",
|
||||
cwd: tempDir,
|
||||
authProfileId: "openai-codex:work",
|
||||
model: "gpt-5.4-mini",
|
||||
modelProvider: "openai",
|
||||
},
|
||||
{
|
||||
authProfileStore: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:work": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const binding = await readCodexAppServerBinding(sessionFile, {
|
||||
authProfileStore: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:work": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-test",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(binding?.modelProvider).toBe("openai");
|
||||
});
|
||||
|
||||
it("clears missing bindings without throwing", async () => {
|
||||
const sessionFile = path.join(tempDir, "missing.json");
|
||||
await clearCodexAppServerBinding(sessionFile);
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
resolveOpenClawAgentDir,
|
||||
resolveProviderIdForAuth,
|
||||
type AuthProfileStore,
|
||||
} from "openclaw/plugin-sdk/agent-runtime";
|
||||
import type { CodexAppServerApprovalPolicy, CodexAppServerSandboxMode } from "./config.js";
|
||||
import type { CodexServiceTier } from "./protocol.js";
|
||||
|
||||
const CODEX_APP_SERVER_NATIVE_AUTH_PROVIDER = "openai-codex";
|
||||
const PUBLIC_OPENAI_MODEL_PROVIDER = "openai";
|
||||
|
||||
type ProviderAuthAliasLookupParams = Parameters<typeof resolveProviderIdForAuth>[1];
|
||||
type ProviderAuthAliasConfig = NonNullable<ProviderAuthAliasLookupParams>["config"];
|
||||
|
||||
export type CodexAppServerAuthProfileLookup = {
|
||||
authProfileId?: string;
|
||||
authProfileStore?: AuthProfileStore;
|
||||
agentDir?: string;
|
||||
config?: ProviderAuthAliasConfig;
|
||||
};
|
||||
|
||||
export type CodexAppServerThreadBinding = {
|
||||
schemaVersion: 1;
|
||||
threadId: string;
|
||||
@@ -28,6 +44,7 @@ export function resolveCodexAppServerBindingPath(sessionFile: string): string {
|
||||
|
||||
export async function readCodexAppServerBinding(
|
||||
sessionFile: string,
|
||||
lookup: Omit<CodexAppServerAuthProfileLookup, "authProfileId"> = {},
|
||||
): Promise<CodexAppServerThreadBinding | undefined> {
|
||||
const path = resolveCodexAppServerBindingPath(sessionFile);
|
||||
let raw: string;
|
||||
@@ -45,15 +62,18 @@ export async function readCodexAppServerBinding(
|
||||
if (parsed.schemaVersion !== 1 || typeof parsed.threadId !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const authProfileId =
|
||||
typeof parsed.authProfileId === "string" ? parsed.authProfileId : undefined;
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
threadId: parsed.threadId,
|
||||
sessionFile,
|
||||
cwd: typeof parsed.cwd === "string" ? parsed.cwd : "",
|
||||
authProfileId: typeof parsed.authProfileId === "string" ? parsed.authProfileId : undefined,
|
||||
authProfileId,
|
||||
model: typeof parsed.model === "string" ? parsed.model : undefined,
|
||||
modelProvider: normalizeCodexAppServerBindingModelProvider({
|
||||
authProfileId: typeof parsed.authProfileId === "string" ? parsed.authProfileId : undefined,
|
||||
...lookup,
|
||||
authProfileId,
|
||||
modelProvider: typeof parsed.modelProvider === "string" ? parsed.modelProvider : undefined,
|
||||
}),
|
||||
approvalPolicy: readApprovalPolicy(parsed.approvalPolicy),
|
||||
@@ -80,6 +100,7 @@ export async function writeCodexAppServerBinding(
|
||||
> & {
|
||||
createdAt?: string;
|
||||
},
|
||||
lookup: Omit<CodexAppServerAuthProfileLookup, "authProfileId"> = {},
|
||||
): Promise<void> {
|
||||
const now = new Date().toISOString();
|
||||
const payload: CodexAppServerThreadBinding = {
|
||||
@@ -90,6 +111,7 @@ export async function writeCodexAppServerBinding(
|
||||
authProfileId: binding.authProfileId,
|
||||
model: binding.model,
|
||||
modelProvider: normalizeCodexAppServerBindingModelProvider({
|
||||
...lookup,
|
||||
authProfileId: binding.authProfileId,
|
||||
modelProvider: binding.modelProvider,
|
||||
}),
|
||||
@@ -120,25 +142,44 @@ function isNotFound(error: unknown): boolean {
|
||||
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
||||
}
|
||||
|
||||
export function isCodexAppServerNativeAuthProfileId(authProfileId: string | undefined): boolean {
|
||||
const normalized = authProfileId?.trim().toLowerCase();
|
||||
return Boolean(
|
||||
normalized &&
|
||||
(normalized === CODEX_APP_SERVER_NATIVE_AUTH_PROVIDER ||
|
||||
normalized.startsWith(`${CODEX_APP_SERVER_NATIVE_AUTH_PROVIDER}:`)),
|
||||
);
|
||||
export function isCodexAppServerNativeAuthProfile(
|
||||
lookup: CodexAppServerAuthProfileLookup,
|
||||
): boolean {
|
||||
const authProfileId = lookup.authProfileId?.trim();
|
||||
if (!authProfileId) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const credential = resolveCodexAppServerAuthProfileCredential({
|
||||
...lookup,
|
||||
authProfileId,
|
||||
});
|
||||
return isCodexAppServerNativeAuthProvider({
|
||||
provider: credential?.provider,
|
||||
config: lookup.config,
|
||||
});
|
||||
} catch (error) {
|
||||
embeddedAgentLog.debug("failed to resolve codex app-server auth profile provider", {
|
||||
authProfileId,
|
||||
error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeCodexAppServerBindingModelProvider(params: {
|
||||
authProfileId?: string;
|
||||
modelProvider?: string;
|
||||
authProfileStore?: AuthProfileStore;
|
||||
agentDir?: string;
|
||||
config?: ProviderAuthAliasConfig;
|
||||
}): string | undefined {
|
||||
const modelProvider = params.modelProvider?.trim();
|
||||
if (!modelProvider) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
isCodexAppServerNativeAuthProfileId(params.authProfileId) &&
|
||||
isCodexAppServerNativeAuthProfile(params) &&
|
||||
modelProvider.toLowerCase() === PUBLIC_OPENAI_MODEL_PROVIDER
|
||||
) {
|
||||
return undefined;
|
||||
@@ -146,6 +187,35 @@ export function normalizeCodexAppServerBindingModelProvider(params: {
|
||||
return modelProvider;
|
||||
}
|
||||
|
||||
function resolveCodexAppServerAuthProfileCredential(
|
||||
lookup: CodexAppServerAuthProfileLookup,
|
||||
): AuthProfileStore["profiles"][string] | undefined {
|
||||
const authProfileId = lookup.authProfileId?.trim();
|
||||
if (!authProfileId) {
|
||||
return undefined;
|
||||
}
|
||||
const store = lookup.authProfileStore ?? loadCodexAppServerAuthProfileStore(lookup.agentDir);
|
||||
return store.profiles[authProfileId];
|
||||
}
|
||||
|
||||
function loadCodexAppServerAuthProfileStore(agentDir: string | undefined): AuthProfileStore {
|
||||
return ensureAuthProfileStore(agentDir?.trim() || resolveOpenClawAgentDir(), {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
}
|
||||
|
||||
function isCodexAppServerNativeAuthProvider(params: {
|
||||
provider?: string;
|
||||
config?: ProviderAuthAliasConfig;
|
||||
}): boolean {
|
||||
const provider = params.provider?.trim();
|
||||
return Boolean(
|
||||
provider &&
|
||||
resolveProviderIdForAuth(provider, { config: params.config }) ===
|
||||
CODEX_APP_SERVER_NATIVE_AUTH_PROVIDER,
|
||||
);
|
||||
}
|
||||
|
||||
function readApprovalPolicy(value: unknown): CodexAppServerApprovalPolicy | undefined {
|
||||
return value === "never" ||
|
||||
value === "on-request" ||
|
||||
|
||||
@@ -9,11 +9,33 @@ import {
|
||||
function createAttemptParams(params: {
|
||||
provider: string;
|
||||
authProfileId?: string;
|
||||
authProfileProvider?: string;
|
||||
authProfileProviders?: Record<string, string>;
|
||||
}): EmbeddedRunAttemptParams {
|
||||
const authProfileProviders =
|
||||
params.authProfileProviders ??
|
||||
(params.authProfileId
|
||||
? { [params.authProfileId]: params.authProfileProvider ?? "openai-codex" }
|
||||
: {});
|
||||
return {
|
||||
provider: params.provider,
|
||||
modelId: "gpt-5.4",
|
||||
authProfileId: params.authProfileId,
|
||||
authProfileStore: {
|
||||
version: 1,
|
||||
profiles: Object.fromEntries(
|
||||
Object.entries(authProfileProviders).map(([profileId, provider]) => [
|
||||
profileId,
|
||||
{
|
||||
type: "oauth" as const,
|
||||
provider,
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
} as EmbeddedRunAttemptParams;
|
||||
}
|
||||
|
||||
@@ -30,7 +52,7 @@ describe("Codex app-server model provider selection", () => {
|
||||
"omits public %s modelProvider when forwarding native Codex auth on thread/start",
|
||||
(provider) => {
|
||||
const request = buildThreadStartParams(
|
||||
createAttemptParams({ provider, authProfileId: "openai-codex:work" }),
|
||||
createAttemptParams({ provider, authProfileId: "work" }),
|
||||
{
|
||||
cwd: "/repo",
|
||||
dynamicTools: [],
|
||||
@@ -44,16 +66,40 @@ describe("Codex app-server model provider selection", () => {
|
||||
);
|
||||
|
||||
it("uses the bound native Codex auth profile when deciding thread/resume modelProvider", () => {
|
||||
const request = buildThreadResumeParams(createAttemptParams({ provider: "openai" }), {
|
||||
threadId: "thread-1",
|
||||
authProfileId: "openai-codex:bound",
|
||||
appServer: createAppServerOptions() as never,
|
||||
developerInstructions: "test instructions",
|
||||
});
|
||||
const request = buildThreadResumeParams(
|
||||
createAttemptParams({
|
||||
provider: "openai",
|
||||
authProfileProviders: { bound: "openai-codex" },
|
||||
}),
|
||||
{
|
||||
threadId: "thread-1",
|
||||
authProfileId: "bound",
|
||||
appServer: createAppServerOptions() as never,
|
||||
developerInstructions: "test instructions",
|
||||
},
|
||||
);
|
||||
|
||||
expect(request).not.toHaveProperty("modelProvider");
|
||||
});
|
||||
|
||||
it("does not infer native Codex auth from the profile id prefix", () => {
|
||||
const request = buildThreadStartParams(
|
||||
createAttemptParams({
|
||||
provider: "openai",
|
||||
authProfileId: "openai-codex:work",
|
||||
authProfileProvider: "openai",
|
||||
}),
|
||||
{
|
||||
cwd: "/repo",
|
||||
dynamicTools: [],
|
||||
appServer: createAppServerOptions() as never,
|
||||
developerInstructions: "test instructions",
|
||||
},
|
||||
);
|
||||
|
||||
expect(request).toMatchObject({ modelProvider: "openai" });
|
||||
});
|
||||
|
||||
it("keeps public OpenAI modelProvider when no native Codex auth profile is selected", () => {
|
||||
const request = buildThreadStartParams(createAttemptParams({ provider: "openai" }), {
|
||||
cwd: "/repo",
|
||||
|
||||
@@ -25,9 +25,10 @@ import {
|
||||
} from "./protocol.js";
|
||||
import {
|
||||
clearCodexAppServerBinding,
|
||||
isCodexAppServerNativeAuthProfileId,
|
||||
isCodexAppServerNativeAuthProfile,
|
||||
readCodexAppServerBinding,
|
||||
writeCodexAppServerBinding,
|
||||
type CodexAppServerAuthProfileLookup,
|
||||
type CodexAppServerThreadBinding,
|
||||
} from "./session-binding.js";
|
||||
|
||||
@@ -41,7 +42,11 @@ export async function startOrResumeThread(params: {
|
||||
config?: JsonObject;
|
||||
}): Promise<CodexAppServerThreadBinding> {
|
||||
const dynamicToolsFingerprint = fingerprintDynamicTools(params.dynamicTools);
|
||||
const binding = await readCodexAppServerBinding(params.params.sessionFile);
|
||||
const binding = await readCodexAppServerBinding(params.params.sessionFile, {
|
||||
authProfileStore: params.params.authProfileStore,
|
||||
agentDir: params.params.agentDir,
|
||||
config: params.params.config,
|
||||
});
|
||||
if (binding?.threadId) {
|
||||
// `/codex resume <thread>` writes a binding before the next turn can know
|
||||
// the dynamic tool catalog, so only invalidate fingerprints we actually have.
|
||||
@@ -75,16 +80,27 @@ export async function startOrResumeThread(params: {
|
||||
const fallbackModelProvider = resolveCodexAppServerModelProvider({
|
||||
provider: params.params.provider,
|
||||
authProfileId: boundAuthProfileId,
|
||||
authProfileStore: params.params.authProfileStore,
|
||||
agentDir: params.params.agentDir,
|
||||
config: params.params.config,
|
||||
});
|
||||
await writeCodexAppServerBinding(params.params.sessionFile, {
|
||||
threadId: response.thread.id,
|
||||
cwd: params.cwd,
|
||||
authProfileId: boundAuthProfileId,
|
||||
model: params.params.modelId,
|
||||
modelProvider: response.modelProvider ?? fallbackModelProvider,
|
||||
dynamicToolsFingerprint,
|
||||
createdAt: binding.createdAt,
|
||||
});
|
||||
await writeCodexAppServerBinding(
|
||||
params.params.sessionFile,
|
||||
{
|
||||
threadId: response.thread.id,
|
||||
cwd: params.cwd,
|
||||
authProfileId: boundAuthProfileId,
|
||||
model: params.params.modelId,
|
||||
modelProvider: response.modelProvider ?? fallbackModelProvider,
|
||||
dynamicToolsFingerprint,
|
||||
createdAt: binding.createdAt,
|
||||
},
|
||||
{
|
||||
authProfileStore: params.params.authProfileStore,
|
||||
agentDir: params.params.agentDir,
|
||||
config: params.params.config,
|
||||
},
|
||||
);
|
||||
return {
|
||||
...binding,
|
||||
threadId: response.thread.id,
|
||||
@@ -121,17 +137,28 @@ export async function startOrResumeThread(params: {
|
||||
const modelProvider = resolveCodexAppServerModelProvider({
|
||||
provider: params.params.provider,
|
||||
authProfileId: params.params.authProfileId,
|
||||
authProfileStore: params.params.authProfileStore,
|
||||
agentDir: params.params.agentDir,
|
||||
config: params.params.config,
|
||||
});
|
||||
const createdAt = new Date().toISOString();
|
||||
await writeCodexAppServerBinding(params.params.sessionFile, {
|
||||
threadId: response.thread.id,
|
||||
cwd: params.cwd,
|
||||
authProfileId: params.params.authProfileId,
|
||||
model: response.model ?? params.params.modelId,
|
||||
modelProvider: response.modelProvider ?? modelProvider,
|
||||
dynamicToolsFingerprint,
|
||||
createdAt,
|
||||
});
|
||||
await writeCodexAppServerBinding(
|
||||
params.params.sessionFile,
|
||||
{
|
||||
threadId: response.thread.id,
|
||||
cwd: params.cwd,
|
||||
authProfileId: params.params.authProfileId,
|
||||
model: response.model ?? params.params.modelId,
|
||||
modelProvider: response.modelProvider ?? modelProvider,
|
||||
dynamicToolsFingerprint,
|
||||
createdAt,
|
||||
},
|
||||
{
|
||||
authProfileStore: params.params.authProfileStore,
|
||||
agentDir: params.params.agentDir,
|
||||
config: params.params.config,
|
||||
},
|
||||
);
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
threadId: response.thread.id,
|
||||
@@ -159,6 +186,9 @@ export function buildThreadStartParams(
|
||||
const modelProvider = resolveCodexAppServerModelProvider({
|
||||
provider: params.provider,
|
||||
authProfileId: params.authProfileId,
|
||||
authProfileStore: params.authProfileStore,
|
||||
agentDir: params.agentDir,
|
||||
config: params.config,
|
||||
});
|
||||
return {
|
||||
model: params.modelId,
|
||||
@@ -190,6 +220,9 @@ export function buildThreadResumeParams(
|
||||
const modelProvider = resolveCodexAppServerModelProvider({
|
||||
provider: params.provider,
|
||||
authProfileId: options.authProfileId ?? params.authProfileId,
|
||||
authProfileStore: params.authProfileStore,
|
||||
agentDir: params.agentDir,
|
||||
config: params.config,
|
||||
});
|
||||
return {
|
||||
threadId: options.threadId,
|
||||
@@ -345,6 +378,9 @@ function buildUserInput(
|
||||
function resolveCodexAppServerModelProvider(params: {
|
||||
provider: string;
|
||||
authProfileId?: string;
|
||||
authProfileStore?: CodexAppServerAuthProfileLookup["authProfileStore"];
|
||||
agentDir?: string;
|
||||
config?: CodexAppServerAuthProfileLookup["config"];
|
||||
}): string | undefined {
|
||||
const normalized = params.provider.trim();
|
||||
const normalizedLower = normalized.toLowerCase();
|
||||
@@ -354,7 +390,7 @@ function resolveCodexAppServerModelProvider(params: {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
isCodexAppServerNativeAuthProfileId(params.authProfileId) &&
|
||||
isCodexAppServerNativeAuthProfile(params) &&
|
||||
(normalizedLower === "openai" || normalizedLower === "openai-codex")
|
||||
) {
|
||||
// When OpenClaw is forwarding ChatGPT/Codex OAuth, forcing the public
|
||||
|
||||
@@ -106,13 +106,25 @@ describe("codex conversation binding", () => {
|
||||
|
||||
it("preserves Codex auth and omits the public OpenAI provider for native bind threads", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {
|
||||
work: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fs.writeFile(
|
||||
`${sessionFile}.codex-app-server.json`,
|
||||
JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
threadId: "thread-old",
|
||||
cwd: tempDir,
|
||||
authProfileId: "openai-codex:work",
|
||||
authProfileId: "work",
|
||||
modelProvider: "openai",
|
||||
}),
|
||||
);
|
||||
@@ -136,7 +148,7 @@ describe("codex conversation binding", () => {
|
||||
});
|
||||
|
||||
expect(sharedClientMocks.getSharedCodexAppServerClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ authProfileId: "openai-codex:work" }),
|
||||
expect.objectContaining({ authProfileId: "work" }),
|
||||
);
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(requests[0]).toMatchObject({
|
||||
@@ -145,7 +157,7 @@ describe("codex conversation binding", () => {
|
||||
});
|
||||
expect(requests[0]?.params).not.toHaveProperty("modelProvider");
|
||||
await expect(fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8")).resolves.toContain(
|
||||
'"authProfileId": "openai-codex:work"',
|
||||
'"authProfileId": "work"',
|
||||
);
|
||||
await expect(
|
||||
fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8"),
|
||||
|
||||
@@ -19,10 +19,11 @@ import {
|
||||
} from "./app-server/protocol.js";
|
||||
import {
|
||||
clearCodexAppServerBinding,
|
||||
isCodexAppServerNativeAuthProfileId,
|
||||
isCodexAppServerNativeAuthProfile,
|
||||
normalizeCodexAppServerBindingModelProvider,
|
||||
readCodexAppServerBinding,
|
||||
writeCodexAppServerBinding,
|
||||
type CodexAppServerAuthProfileLookup,
|
||||
} from "./app-server/session-binding.js";
|
||||
import { getSharedCodexAppServerClient } from "./app-server/shared-client.js";
|
||||
import {
|
||||
@@ -82,7 +83,9 @@ export async function startCodexConversationThread(
|
||||
): Promise<CodexConversationBindingData> {
|
||||
const workspaceDir =
|
||||
params.workspaceDir?.trim() || resolveCodexDefaultWorkspaceDir(params.pluginConfig);
|
||||
const existingBinding = await readCodexAppServerBinding(params.sessionFile);
|
||||
const existingBinding = await readCodexAppServerBinding(params.sessionFile, {
|
||||
config: params.config,
|
||||
});
|
||||
const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
|
||||
authProfileId: params.authProfileId ?? existingBinding?.authProfileId,
|
||||
config: params.config,
|
||||
@@ -96,6 +99,7 @@ export async function startCodexConversationThread(
|
||||
model: params.model,
|
||||
modelProvider: params.modelProvider,
|
||||
authProfileId,
|
||||
config: params.config,
|
||||
});
|
||||
} else {
|
||||
await createThread({
|
||||
@@ -105,6 +109,7 @@ export async function startCodexConversationThread(
|
||||
model: params.model,
|
||||
modelProvider: params.modelProvider,
|
||||
authProfileId,
|
||||
config: params.config,
|
||||
});
|
||||
}
|
||||
return createCodexConversationBindingData({
|
||||
@@ -171,11 +176,13 @@ async function attachExistingThread(params: {
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
authProfileId?: string;
|
||||
config?: CodexAppServerAuthProfileLookup["config"];
|
||||
}): Promise<void> {
|
||||
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
|
||||
const modelProvider = resolveThreadRequestModelProvider({
|
||||
authProfileId: params.authProfileId,
|
||||
modelProvider: params.modelProvider,
|
||||
config: params.config,
|
||||
});
|
||||
const client = await getSharedCodexAppServerClient({
|
||||
startOptions: runtime.start,
|
||||
@@ -197,19 +204,26 @@ async function attachExistingThread(params: {
|
||||
{ timeoutMs: runtime.requestTimeoutMs },
|
||||
);
|
||||
const thread = response.thread;
|
||||
await writeCodexAppServerBinding(params.sessionFile, {
|
||||
threadId: thread.id,
|
||||
cwd: thread.cwd ?? params.workspaceDir,
|
||||
authProfileId: params.authProfileId,
|
||||
model: response.model ?? params.model,
|
||||
modelProvider: normalizeCodexAppServerBindingModelProvider({
|
||||
await writeCodexAppServerBinding(
|
||||
params.sessionFile,
|
||||
{
|
||||
threadId: thread.id,
|
||||
cwd: thread.cwd ?? params.workspaceDir,
|
||||
authProfileId: params.authProfileId,
|
||||
modelProvider: response.modelProvider ?? params.modelProvider,
|
||||
}),
|
||||
approvalPolicy: runtime.approvalPolicy,
|
||||
sandbox: runtime.sandbox,
|
||||
serviceTier: runtime.serviceTier,
|
||||
});
|
||||
model: response.model ?? params.model,
|
||||
modelProvider: normalizeCodexAppServerBindingModelProvider({
|
||||
config: params.config,
|
||||
authProfileId: params.authProfileId,
|
||||
modelProvider: response.modelProvider ?? params.modelProvider,
|
||||
}),
|
||||
approvalPolicy: runtime.approvalPolicy,
|
||||
sandbox: runtime.sandbox,
|
||||
serviceTier: runtime.serviceTier,
|
||||
},
|
||||
{
|
||||
config: params.config,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function createThread(params: {
|
||||
@@ -219,11 +233,13 @@ async function createThread(params: {
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
authProfileId?: string;
|
||||
config?: CodexAppServerAuthProfileLookup["config"];
|
||||
}): Promise<void> {
|
||||
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
|
||||
const modelProvider = resolveThreadRequestModelProvider({
|
||||
authProfileId: params.authProfileId,
|
||||
modelProvider: params.modelProvider,
|
||||
config: params.config,
|
||||
});
|
||||
const client = await getSharedCodexAppServerClient({
|
||||
startOptions: runtime.start,
|
||||
@@ -247,19 +263,26 @@ async function createThread(params: {
|
||||
},
|
||||
{ timeoutMs: runtime.requestTimeoutMs },
|
||||
);
|
||||
await writeCodexAppServerBinding(params.sessionFile, {
|
||||
threadId: response.thread.id,
|
||||
cwd: response.thread.cwd ?? params.workspaceDir,
|
||||
authProfileId: params.authProfileId,
|
||||
model: response.model ?? params.model,
|
||||
modelProvider: normalizeCodexAppServerBindingModelProvider({
|
||||
await writeCodexAppServerBinding(
|
||||
params.sessionFile,
|
||||
{
|
||||
threadId: response.thread.id,
|
||||
cwd: response.thread.cwd ?? params.workspaceDir,
|
||||
authProfileId: params.authProfileId,
|
||||
modelProvider: response.modelProvider ?? params.modelProvider,
|
||||
}),
|
||||
approvalPolicy: runtime.approvalPolicy,
|
||||
sandbox: runtime.sandbox,
|
||||
serviceTier: runtime.serviceTier,
|
||||
});
|
||||
model: response.model ?? params.model,
|
||||
modelProvider: normalizeCodexAppServerBindingModelProvider({
|
||||
config: params.config,
|
||||
authProfileId: params.authProfileId,
|
||||
modelProvider: response.modelProvider ?? params.modelProvider,
|
||||
}),
|
||||
approvalPolicy: runtime.approvalPolicy,
|
||||
sandbox: runtime.sandbox,
|
||||
serviceTier: runtime.serviceTier,
|
||||
},
|
||||
{
|
||||
config: params.config,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function runBoundTurn(params: {
|
||||
@@ -387,13 +410,14 @@ function enqueueBoundTurn<T>(key: string, run: () => Promise<T>): Promise<T> {
|
||||
function resolveThreadRequestModelProvider(params: {
|
||||
authProfileId?: string;
|
||||
modelProvider?: string;
|
||||
config?: CodexAppServerAuthProfileLookup["config"];
|
||||
}): string | undefined {
|
||||
const modelProvider = params.modelProvider?.trim();
|
||||
if (!modelProvider || modelProvider.toLowerCase() === "codex") {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
isCodexAppServerNativeAuthProfileId(params.authProfileId) &&
|
||||
isCodexAppServerNativeAuthProfile(params) &&
|
||||
(modelProvider.toLowerCase() === "openai" || modelProvider.toLowerCase() === "openai-codex")
|
||||
) {
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user