Skip to content

Commit 1017e31

Browse files
committed
fix(openai): align auth login metadata and fast tests
1 parent 6643695 commit 1017e31

15 files changed

Lines changed: 184 additions & 82 deletions

extensions/openai/openai-provider.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,10 @@ describe("buildOpenAIProvider", () => {
129129

130130
expectFields(apiKey?.wizard, {
131131
choiceLabel: "OpenAI API Key",
132+
choiceHint: "Use your OpenAI API key directly",
132133
groupId: "openai",
133134
groupLabel: "OpenAI",
134-
groupHint: "Direct API key",
135+
groupHint: "ChatGPT subscription or API key",
135136
});
136137
});
137138

extensions/openai/openai-provider.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
type ProviderPlugin,
1111
} from "openclaw/plugin-sdk/provider-model-shared";
1212
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
13-
import { OPENAI_API_KEY_LABEL, OPENAI_API_KEY_WIZARD_GROUP } from "./auth-choice-copy.js";
13+
import { OPENAI_ACCOUNT_WIZARD_GROUP, OPENAI_API_KEY_LABEL } from "./auth-choice-copy.js";
1414
import { isOpenAIApiBaseUrl } from "./base-url.js";
1515
import { applyOpenAIConfig, OPENAI_DEFAULT_MODEL } from "./default-models.js";
1616
import {
@@ -233,7 +233,9 @@ export function buildOpenAIProvider(): ProviderPlugin {
233233
wizard: {
234234
choiceId: "openai-api-key",
235235
choiceLabel: OPENAI_API_KEY_LABEL,
236-
...OPENAI_API_KEY_WIZARD_GROUP,
236+
choiceHint: "Use your OpenAI API key directly",
237+
assistantPriority: 5,
238+
...OPENAI_ACCOUNT_WIZARD_GROUP,
237239
},
238240
}),
239241
],

extensions/openai/openclaw.plugin.json

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,28 @@
761761
"openai": ["OPENAI_API_KEY"]
762762
},
763763
"providerAuthChoices": [
764+
{
765+
"provider": "openai",
766+
"method": "oauth",
767+
"choiceId": "openai",
768+
"choiceLabel": "ChatGPT Login",
769+
"choiceHint": "Sign in with your ChatGPT or Codex subscription",
770+
"assistantPriority": -40,
771+
"groupId": "openai",
772+
"groupLabel": "OpenAI",
773+
"groupHint": "ChatGPT subscription or API key"
774+
},
775+
{
776+
"provider": "openai",
777+
"method": "device-code",
778+
"choiceId": "openai-device-code",
779+
"choiceLabel": "ChatGPT Device Pairing",
780+
"choiceHint": "Pair your ChatGPT account in browser with a device code",
781+
"assistantPriority": -10,
782+
"groupId": "openai",
783+
"groupLabel": "OpenAI",
784+
"groupHint": "ChatGPT subscription or API key"
785+
},
764786
{
765787
"provider": "openai-codex",
766788
"method": "oauth",
@@ -804,10 +826,11 @@
804826
"method": "api-key",
805827
"choiceId": "openai-api-key",
806828
"choiceLabel": "OpenAI API Key",
807-
"assistantPriority": -40,
829+
"choiceHint": "Use your OpenAI API key directly",
830+
"assistantPriority": 5,
808831
"groupId": "openai",
809832
"groupLabel": "OpenAI",
810-
"groupHint": "Direct API key",
833+
"groupHint": "ChatGPT subscription or API key",
811834
"optionKey": "openaiApiKey",
812835
"cliFlag": "--openai-api-key",
813836
"cliOption": "--openai-api-key <key>",

extensions/openai/openclaw.plugin.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
22
import { describe, expect, it } from "vitest";
33
import { buildOpenAICodexProviderPlugin } from "./openai-codex-provider.js";
44
import { buildOpenAIProvider } from "./openai-provider.js";
5+
import { buildOpenAICodexSetupProvider, buildOpenAISetupProvider } from "./setup-api.js";
56

67
const manifest = JSON.parse(
78
readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf8"),
@@ -54,7 +55,12 @@ function manifestComparableWizardFields(choice: {
5455
}
5556

5657
function providerWizardByKey() {
57-
const providers = [buildOpenAIProvider(), buildOpenAICodexProviderPlugin()];
58+
const providers = [
59+
buildOpenAIProvider(),
60+
buildOpenAICodexProviderPlugin(),
61+
buildOpenAISetupProvider(),
62+
buildOpenAICodexSetupProvider(),
63+
];
5864
const wizards = new Map<string, Record<string, unknown>>();
5965

6066
for (const provider of providers) {
@@ -110,11 +116,25 @@ describe("OpenAI plugin manifest", () => {
110116
const codexDeviceCode = choices.find(
111117
(choice) => choice.choiceId === "openai-codex-device-code",
112118
);
119+
const openAiLogin = choices.find((choice) => choice.choiceId === "openai");
120+
const openAiDeviceCode = choices.find((choice) => choice.choiceId === "openai-device-code");
113121
const apiKey = choices.find(
114122
(choice) => choice.provider === "openai" && choice.method === "api-key",
115123
);
116124
const codexApiKey = choices.find((choice) => choice.choiceId === "openai-codex-api-key");
117125

126+
expect(openAiLogin?.choiceLabel).toBe("ChatGPT Login");
127+
expect(openAiLogin?.choiceHint).toBe("Sign in with your ChatGPT or Codex subscription");
128+
expect(openAiLogin?.groupId).toBe("openai");
129+
expect(openAiLogin?.groupLabel).toBe("OpenAI");
130+
expect(openAiLogin?.groupHint).toBe("ChatGPT subscription or API key");
131+
expect(openAiDeviceCode?.choiceLabel).toBe("ChatGPT Device Pairing");
132+
expect(openAiDeviceCode?.choiceHint).toBe(
133+
"Pair your ChatGPT account in browser with a device code",
134+
);
135+
expect(openAiDeviceCode?.groupId).toBe("openai");
136+
expect(openAiDeviceCode?.groupLabel).toBe("OpenAI");
137+
expect(openAiDeviceCode?.groupHint).toBe("ChatGPT subscription or API key");
118138
expect(codexBrowserLogin?.choiceLabel).toBe("OpenAI Codex Browser Login");
119139
expect(codexBrowserLogin?.choiceHint).toBe("Sign in with OpenAI in your browser");
120140
expect(codexBrowserLogin?.groupId).toBe("openai-codex");
@@ -126,9 +146,10 @@ describe("OpenAI plugin manifest", () => {
126146
expect(codexDeviceCode?.groupLabel).toBe("OpenAI Codex");
127147
expect(codexDeviceCode?.groupHint).toBe("ChatGPT/Codex sign-in");
128148
expect(apiKey?.choiceLabel).toBe("OpenAI API Key");
149+
expect(apiKey?.choiceHint).toBe("Use your OpenAI API key directly");
129150
expect(apiKey?.groupId).toBe("openai");
130151
expect(apiKey?.groupLabel).toBe("OpenAI");
131-
expect(apiKey?.groupHint).toBe("Direct API key");
152+
expect(apiKey?.groupHint).toBe("ChatGPT subscription or API key");
132153
expect(codexApiKey?.choiceLabel).toBe("OpenAI API Key Backup");
133154
expect(codexApiKey?.choiceHint).toBe(
134155
"Use an OpenAI API key when your Codex subscription is unavailable",

extensions/openai/provider-contract-api.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
22
import {
3+
OPENAI_ACCOUNT_WIZARD_GROUP,
34
OPENAI_API_KEY_LABEL,
45
OPENAI_CODEX_DEVICE_PAIRING_HINT,
56
OPENAI_CODEX_DEVICE_PAIRING_LABEL,
67
OPENAI_CODEX_LOGIN_HINT,
78
OPENAI_CODEX_LOGIN_LABEL,
8-
OPENAI_API_KEY_WIZARD_GROUP,
99
OPENAI_CODEX_WIZARD_GROUP,
1010
} from "./auth-choice-copy.js";
1111

@@ -72,8 +72,9 @@ export function createOpenAIProvider(): ProviderPlugin {
7272
wizard: {
7373
choiceId: "openai-api-key",
7474
choiceLabel: OPENAI_API_KEY_LABEL,
75-
assistantPriority: -40,
76-
...OPENAI_API_KEY_WIZARD_GROUP,
75+
choiceHint: "Use your OpenAI API key directly",
76+
assistantPriority: 5,
77+
...OPENAI_ACCOUNT_WIZARD_GROUP,
7778
},
7879
},
7980
],

src/commands/models/auth.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,13 +448,54 @@ describe("modelsAuthLoginCommand", () => {
448448

449449
it("defaults OpenAI login to ChatGPT OAuth when API key is also available", async () => {
450450
const runtime = createRuntime();
451-
const runOauthAuth = vi.fn().mockResolvedValue({ profiles: [] });
451+
const fakeStore = {
452+
profiles: {
453+
"openai:api-key-backup": {
454+
type: "api_key",
455+
provider: "openai",
456+
},
457+
"openai-codex:[email protected]": {
458+
type: "oauth",
459+
provider: "openai-codex",
460+
},
461+
},
462+
usageStats: {
463+
"openai-codex:[email protected]": {
464+
disabledUntil: Date.now() + 3_600_000,
465+
disabledReason: "rate_limit",
466+
errorCount: 1,
467+
},
468+
},
469+
};
470+
const runOauthAuth = vi.fn().mockResolvedValue({
471+
profiles: [
472+
{
473+
profileId: "openai-codex:[email protected]",
474+
credential: {
475+
type: "oauth",
476+
provider: "openai-codex",
477+
access: "access-token",
478+
refresh: "refresh-token",
479+
expires: Date.now() + 60_000,
480+
481+
},
482+
},
483+
],
484+
});
452485
const runApiKeyAuth = vi.fn().mockResolvedValue({ profiles: [] });
453486
const select = vi.fn();
454487
mocks.createClackPrompter.mockReturnValue({
455488
note: vi.fn(async () => {}),
456489
select,
457490
});
491+
mocks.loadAuthProfileStoreForRuntime.mockReturnValue(fakeStore);
492+
mocks.listProfilesForProvider.mockImplementation((_store: unknown, provider: string) =>
493+
provider === "openai"
494+
? ["openai:api-key-backup"]
495+
: provider === "openai-codex"
496+
? ["openai-codex:[email protected]"]
497+
: [],
498+
);
458499
mocks.resolvePluginProviders.mockReturnValue([
459500
createProvider({
460501
id: "openai",
@@ -482,6 +523,16 @@ describe("modelsAuthLoginCommand", () => {
482523
expect(runOauthAuth).toHaveBeenCalledOnce();
483524
expect(runApiKeyAuth).not.toHaveBeenCalled();
484525
expect(select).not.toHaveBeenCalled();
526+
expect(mocks.clearAuthProfileCooldown).toHaveBeenCalledWith({
527+
store: fakeStore,
528+
profileId: "openai:api-key-backup",
529+
agentDir: "/tmp/openclaw/agents/main",
530+
});
531+
expect(mocks.clearAuthProfileCooldown).toHaveBeenCalledWith({
532+
store: fakeStore,
533+
profileId: "openai-codex:[email protected]",
534+
agentDir: "/tmp/openclaw/agents/main",
535+
});
485536
});
486537

487538
it("honors --method api-key for OpenAI login", async () => {

src/commands/models/auth.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,8 @@ async function runProviderAuthMethod(params: {
327327
prompter: ReturnType<typeof createClackPrompter>;
328328
setDefault?: boolean;
329329
}) {
330-
await clearStaleProfileLockouts(params.provider.id, params.agentDir);
330+
const selectedProviderId = normalizeProviderId(params.provider.id);
331+
await clearStaleProfileLockouts(selectedProviderId, params.agentDir);
331332

332333
const result = await params.method.run({
333334
config: params.config,
@@ -346,6 +347,14 @@ async function runProviderAuthMethod(params: {
346347
createVpsAwareHandlers: (runtimeParams) => createVpsAwareOAuthHandlers(runtimeParams),
347348
},
348349
});
350+
const resultProviderIds = new Set(
351+
result.profiles.map((profile) => normalizeProviderId(profile.credential.provider)),
352+
);
353+
for (const providerId of resultProviderIds) {
354+
if (providerId && providerId !== selectedProviderId) {
355+
await clearStaleProfileLockouts(providerId, params.agentDir);
356+
}
357+
}
349358

350359
await persistProviderAuthResult({
351360
result,

src/flows/provider-flow.runtime.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import type { OpenClawConfig } from "../config/types.openclaw.js";
2-
import {
3-
resolveProviderModelPickerEntries,
4-
type ProviderModelPickerEntry,
5-
} from "../plugins/provider-wizard.js";
6-
import { resolvePluginProviders } from "../plugins/providers.runtime.js";
2+
import * as providerWizard from "../plugins/provider-wizard.js";
3+
import type { ProviderModelPickerEntry } from "../plugins/provider-wizard.js";
4+
import * as providersRuntime from "../plugins/providers.runtime.js";
75
import type { ProviderPlugin } from "../plugins/types.js";
86
import { normalizeOptionalString } from "../shared/string-coerce.js";
97
import type { FlowContribution } from "./types.js";
@@ -25,12 +23,13 @@ function resolveProviderDocsById(params?: {
2523
env?: NodeJS.ProcessEnv;
2624
}): Map<string, string> {
2725
return new Map(
28-
resolvePluginProviders({
29-
config: params?.config,
30-
workspaceDir: params?.workspaceDir,
31-
env: params?.env,
32-
mode: "setup",
33-
})
26+
providersRuntime
27+
.resolvePluginProviders({
28+
config: params?.config,
29+
workspaceDir: params?.workspaceDir,
30+
env: params?.env,
31+
mode: "setup",
32+
})
3433
.filter((provider): provider is ProviderPlugin & { docsPath: string } =>
3534
Boolean(normalizeOptionalString(provider.docsPath)),
3635
)
@@ -55,7 +54,7 @@ export function resolveProviderModelPickerFlowContributions(params?: {
5554
}): ProviderModelPickerFlowContribution[] {
5655
const docsByProvider = resolveProviderDocsById(params ?? {});
5756
return sortFlowContributionsByLabel(
58-
resolveProviderModelPickerEntries(params ?? {}).map((entry) => {
57+
providerWizard.resolveProviderModelPickerEntries(params ?? {}).map((entry) => {
5958
const providerId = entry.value.startsWith("provider-plugin:")
6059
? entry.value.slice("provider-plugin:".length).split(":")[0]
6160
: entry.value;

src/flows/provider-flow.test.ts

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import * as providerAuthChoices from "../plugins/provider-auth-choices.js";
3+
import * as providerInstallCatalog from "../plugins/provider-install-catalog.js";
4+
import * as providerWizard from "../plugins/provider-wizard.js";
5+
import * as providersRuntime from "../plugins/providers.runtime.js";
26

37
type ResolveProviderInstallCatalogEntries =
48
typeof import("../plugins/provider-install-catalog.js").resolveProviderInstallCatalogEntries;
@@ -11,35 +15,26 @@ type ResolveProviderModelPickerEntries =
1115
type ResolvePluginProviders =
1216
typeof import("../plugins/providers.runtime.js").resolvePluginProviders;
1317

14-
const resolveProviderInstallCatalogEntries = vi.hoisted(() =>
15-
vi.fn<ResolveProviderInstallCatalogEntries>(() => []),
16-
);
17-
vi.mock("../plugins/provider-install-catalog.js", () => ({
18-
resolveProviderInstallCatalogEntries,
19-
}));
20-
21-
const resolveManifestProviderAuthChoices = vi.hoisted(() =>
22-
vi.fn<ResolveManifestProviderAuthChoices>(() => []),
23-
);
24-
vi.mock("../plugins/provider-auth-choices.js", () => ({
25-
resolveManifestProviderAuthChoices,
26-
}));
27-
28-
const resolveProviderWizardOptions = vi.hoisted(() =>
29-
vi.fn<ResolveProviderWizardOptions>(() => []),
30-
);
31-
const resolveProviderModelPickerEntries = vi.hoisted(() =>
32-
vi.fn<ResolveProviderModelPickerEntries>(() => []),
33-
);
34-
vi.mock("../plugins/provider-wizard.js", () => ({
35-
resolveProviderWizardOptions,
36-
resolveProviderModelPickerEntries,
37-
}));
38-
39-
const resolvePluginProviders = vi.hoisted(() => vi.fn<ResolvePluginProviders>(() => []));
40-
vi.mock("../plugins/providers.runtime.js", () => ({
41-
resolvePluginProviders,
42-
}));
18+
const resolveProviderInstallCatalogEntries = vi.spyOn(
19+
providerInstallCatalog,
20+
"resolveProviderInstallCatalogEntries",
21+
) as unknown as ReturnType<typeof vi.fn<ResolveProviderInstallCatalogEntries>>;
22+
const resolveManifestProviderAuthChoices = vi.spyOn(
23+
providerAuthChoices,
24+
"resolveManifestProviderAuthChoices",
25+
) as unknown as ReturnType<typeof vi.fn<ResolveManifestProviderAuthChoices>>;
26+
const resolveProviderWizardOptions = vi.spyOn(
27+
providerWizard,
28+
"resolveProviderWizardOptions",
29+
) as unknown as ReturnType<typeof vi.fn<ResolveProviderWizardOptions>>;
30+
const resolveProviderModelPickerEntries = vi.spyOn(
31+
providerWizard,
32+
"resolveProviderModelPickerEntries",
33+
) as unknown as ReturnType<typeof vi.fn<ResolveProviderModelPickerEntries>>;
34+
const resolvePluginProviders = vi.spyOn(
35+
providersRuntime,
36+
"resolvePluginProviders",
37+
) as unknown as ReturnType<typeof vi.fn<ResolvePluginProviders>>;
4338

4439
import { resolveProviderSetupFlowContributions } from "./provider-flow.js";
4540
import { resolveProviderModelPickerFlowContributions } from "./provider-flow.runtime.js";

0 commit comments

Comments
 (0)