Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/desktop/src/entities/release/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export const changelogReleases: readonly ChangelogRelease[] = [
title: "Open any file from chat",
description: "File links in answers open in Files at the linked line even when the file has not changed, and local links render as file chips. Paths written as inline code, such as src/foo.ts:12, become links when the file exists in the checkout.",
},
{
kind: "improved",
title: "One ChatGPT subscription login",
description: "Pi 0.99 signs in with ChatGPT through the OpenAI provider and marks ChatGPT / Codex as legacy, so the Subscription tab now offers a single OpenAI entry. If you already signed in with ChatGPT / Codex, its card stays so you can keep using it or sign out.",
},
{
kind: "improved",
title: "Continuous corners",
Expand Down
45 changes: 45 additions & 0 deletions apps/desktop/src/pages/settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,51 @@ describe("Settings — changelog", () => {
});
});

describe("Settings — subscription providers", () => {
it("shows one ChatGPT login and keeps the legacy codex card only when signed in", async () => {
const authStatus = (codexSignedIn: boolean): typeof providerAuthStatus => ({
...providerAuthStatus,
providers: [
{
id: "openai",
label: "OpenAI",
supportsApiKey: true,
supportsOAuth: true,
mode: "none",
configured: false,
},
{
id: "openai-codex",
label: "ChatGPT / Codex",
supportsApiKey: true,
supportsOAuth: true,
mode: codexSignedIn ? "oauth" : "none",
configured: codexSignedIn,
},
...providerAuthStatus.providers,
],
});

const first = renderSettings("/usage?settings=providers", disabledUpdateStatus, {
authStatus: authStatus(false),
});
expect(
await screen.findByTestId("provider-subscription-openai"),
).toBeVisible();
expect(
screen.queryByTestId("provider-subscription-openai-codex"),
).not.toBeInTheDocument();
first.unmount();

renderSettings("/usage?settings=providers", disabledUpdateStatus, {
authStatus: authStatus(true),
});
expect(
await screen.findByTestId("provider-subscription-openai-codex"),
).toBeVisible();
});
});

describe("Settings — provider connection test", () => {
it("offers Check only for configured providers and shows a single status line", async () => {
const user = userEvent.setup();
Expand Down
17 changes: 9 additions & 8 deletions apps/desktop/src/pages/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@ import { useUpdateStatus } from "@/entities/update/use-update-status";
import { isModelVisible } from "@/entities/model/model-selector/model-selector-logic";
import { invoke, revealProjectInFinder } from "@/shared/runtime";
import type { UpdateStatus } from "@/shared/update-protocol";
import type {
ProviderAuthId,
ProviderAuthStatusItem,
ProviderAuthStatusReport,
ProviderConnectionTestResult,
ProviderFailureKind,
RuntimeModelCapability,
import {
showsSubscriptionCard,
type ProviderAuthId,
type ProviderAuthStatusItem,
type ProviderAuthStatusReport,
type ProviderConnectionTestResult,
type ProviderFailureKind,
type RuntimeModelCapability,
} from "@pace/core";

type AuthTab = "subscription" | "api_key";
Expand Down Expand Up @@ -942,7 +943,7 @@ function SettingsContent({

const providers = statusQuery.data?.providers ?? [];
const subscriptionProviders = useMemo(
() => providers.filter((provider) => provider.supportsOAuth),
() => providers.filter((provider) => showsSubscriptionCard(provider)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 ChatGPT login offers unavailable models

After ChatGPT signs in through openai, the model list falls back to Pi's unfiltered catalog. Account model sources filter ChatGPT plans only under openai-codex and recognize openai API keys only. Users can select models their subscription rejects.

Learn more

Pace filters model catalogs per provider to the signed-in account's allowed models. The new Subscription entry signs users in under openai, but the account-model sources use ChatGPT's account model endpoint only for openai-codex. The openai source identifies only API-key credentials, so its refresh skips filtering an OpenAI OAuth login. The OpenAI catalog remains available and can contain models outside the ChatGPT subscription; choosing one fails when the session calls the provider.

Example: A user signs into ChatGPT under openai and sees a catalog model unavailable on their plan. They select it in the composer, and the model request fails instead of the unavailable model being omitted.

Recommended fix: Extend the openai account-model source to detect OAuth versus API-key credentials, use the ChatGPT account endpoint for OAuth, and retain the existing API-key model-list behavior. Cover both credential types with account-model runtime tests.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 ChatGPT model mislabeled as API

When both ChatGPT providers expose the same model, openai OAuth rows display “OpenAI API.” The channel label still assumes every openai model uses an API key.

Learn more

The model selector adds channel labels when two providers expose the same model. The channel label map says openai is always "OpenAI API", while the model selector selects this label based only on provider ID. The new OAuth entry lets ChatGPT subscriptions use openai, making the label false when an old Codex login also exposes the model.

Example: A user keeps an existing openai-codex subscription and also signs into openai through ChatGPT. If both list gpt-5.5, the selector calls the openai row "OpenAI API" even though it uses the subscription.

Recommended fix: Derive the channel label from the active auth mode as well as provider ID, or remove billing-specific labels when the mode cannot be determined. Test the duplicate-model case with openai OAuth and legacy Codex OAuth.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

[providers],
);
const visibleApiKeyProviders = useMemo(() => {
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/shared/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,8 @@ export function invokeBrowserFallback<T>(command: string, args?: InvokeArgs): Pr
id: "openai",
label: "OpenAI",
supportsApiKey: true,
supportsOAuth: false,
// Pi 0.99 moved ChatGPT subscription OAuth onto `openai`.
supportsOAuth: true,
mode: "api_key",
configured: true,
keyHint: "…dev1",
Expand Down
1 change: 1 addition & 0 deletions docs/release/0.1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

## 改进

- 只保留一个 ChatGPT 订阅登录:Pi 0.99 改由 OpenAI provider 提供 ChatGPT 订阅登录,并把原来的 ChatGPT / Codex 标为旧版。Settings → Providers → Subscription 现在只显示 OpenAI 一个入口;已经用 ChatGPT / Codex 登录过的用户仍会看到该卡片,可以继续使用或登出。
- 连续曲率圆角:界面圆角统一改为 squircle 形状,并调整圆角刻度,使整体圆润程度与之前一致;圆形按钮保持正圆。
- 动态图标更克制:所有动态图标的动画时长统一为 800ms,只在鼠标真正移入时播放,切换页面后不再自动重播,按下时不再缩放。

Expand Down
10 changes: 6 additions & 4 deletions e2e/smoke/s3-provider-settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,18 @@ test.describe("S3: Provider Settings (DF-002)", () => {
testApp.window.getByTestId("provider-api-key-openai-codex"),
).toHaveCount(0);

// Subscription tab: ChatGPT/Codex + Anthropic + Grok (xAI)
// Subscription tab: OpenAI (ChatGPT subscription since Pi 0.99) +
// Anthropic + Grok (xAI). The legacy openai-codex card only appears
// for an existing codex OAuth sign-in; nothing is seeded here.
await settingsTab(testApp.window, "Subscription").click();
for (const provider of ["openai-codex", "anthropic", "xai"]) {
for (const provider of ["openai", "anthropic", "xai"]) {
const card = testApp.window.getByTestId(`provider-subscription-${provider}`);
await expect(card).toBeVisible();
await expect(card.getByTestId(`provider-icon-${provider}`)).toBeVisible();
}
// OpenAI API key and DeepSeek have no subscription card
// The legacy codex provider and DeepSeek have no subscription card
await expect(
testApp.window.getByTestId("provider-subscription-openai"),
testApp.window.getByTestId("provider-subscription-openai-codex"),
).toHaveCount(0);
await expect(
testApp.window.getByTestId("provider-subscription-deepseek"),
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ export {

export {
FEATURED_PROVIDER_ORDER,
LEGACY_SUBSCRIPTION_PROVIDERS,
PROVIDER_DISPLAY_OVERRIDES,
classifyProviderFailure,
describeProviderFailure,
showsSubscriptionCard,
sortProvidersForDisplay,
type ProviderAuthId,
type ProviderAuthMode,
Expand Down
37 changes: 37 additions & 0 deletions packages/core/src/provider-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
classifyProviderFailure,
describeProviderFailure,
showsSubscriptionCard,
sortProvidersForDisplay,
} from "./provider-auth";

Expand All @@ -27,6 +28,42 @@ describe("sortProvidersForDisplay", () => {
});
});

describe("showsSubscriptionCard", () => {
const codex = { id: "openai-codex", supportsOAuth: true };

it("shows the legacy codex card only for an existing OAuth sign-in", () => {
expect(
showsSubscriptionCard({ ...codex, configured: false, mode: "none" }),
).toBe(false);
expect(
showsSubscriptionCard({ ...codex, configured: true, mode: "oauth" }),
).toBe(true);
// Signed in with an API key is not the subscription flow the card offers.
expect(
showsSubscriptionCard({ ...codex, configured: true, mode: "api_key" }),
).toBe(false);
});

it("shows current OAuth providers regardless of sign-in state", () => {
expect(
showsSubscriptionCard({
id: "openai",
supportsOAuth: true,
configured: false,
mode: "none",
}),
).toBe(true);
expect(
showsSubscriptionCard({
id: "deepseek",
supportsOAuth: false,
configured: false,
mode: "none",
}),
).toBe(false);
});
});

describe("classifyProviderFailure", () => {
it("classifies HTTP 401 as auth, including when the body mentions a plan", () => {
expect(classifyProviderFailure(Object.assign(new Error("subscription plan"), { status: 401 }))).toBe(
Expand Down
26 changes: 24 additions & 2 deletions packages/core/src/provider-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,33 @@ export const PROVIDER_DISPLAY_OVERRIDES: Readonly<Record<string, string>> = {
xai: "Grok (xAI)",
};

export const FEATURED_PROVIDER_ORDER: readonly string[] = [
// Pi 0.99 moved ChatGPT subscription login onto `openai` ("Sign in with
// ChatGPT") and marks `openai-codex` as legacy. The codex card only stays
// for users already signed in through it; everyone else gets the `openai`
// entry so the Subscription tab shows one ChatGPT login, not two.
export const LEGACY_SUBSCRIPTION_PROVIDERS: ReadonlySet<string> = new Set([
"openai-codex",
]);

/** Whether the Subscription tab offers this provider's OAuth login. */
export function showsSubscriptionCard(
item: Pick<
ProviderAuthStatusItem,
"id" | "supportsOAuth" | "configured" | "mode"
>,
): boolean {
return (
item.supportsOAuth &&
(!LEGACY_SUBSCRIPTION_PROVIDERS.has(item.id) ||
(item.configured && item.mode === "oauth"))
);
}

export const FEATURED_PROVIDER_ORDER: readonly string[] = [
"openai",
"anthropic",
"radius",
"openai",
"openai-codex",
"deepseek",
"xai",
"google",
Expand Down
Loading