Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f4d4a05
fix(memory): route auto-memory recall selector to fast model
B-A-M-N May 5, 2026
97688e7
feat(cli): polish --add-dir / --include-directories feature
B-A-M-N May 5, 2026
fc44af6
fix(directory): address review comments on /directory remove command
B-A-M-N May 6, 2026
112d5ff
fix(i18n): add zh and zh-TW translations for /directory remove command
B-A-M-N May 6, 2026
5e306fc
fix(core): retry API request on model-unloaded errors for local model…
B-A-M-N May 7, 2026
5a58502
fix(cli): MCP add/remove now correctly persists headers and server de…
B-A-M-N May 7, 2026
d09381c
fix(cli): merge origin/main into fix/mcp-add-remove-persist
B-A-M-N May 8, 2026
e31e1c8
fix(cli): address review comments on MCP persist and directory remove
B-A-M-N May 8, 2026
b9a8206
fix: address PR #3937 review feedback
B-A-M-N May 8, 2026
f654062
refactor(cli): provider-first auth registry with unified install pipe…
pomelo-nwu May 8, 2026
876c260
fix(core): split prior-read enforcement: partial-OK for Edit, full-re…
wenshao May 8, 2026
d8354b2
fix(cli,core): live-phase panel-ownership filter + post-delete status…
wenshao May 8, 2026
4c07b97
fix(core): close bound-tool gap on runForkedAgent's YOLO wrapper (#3892)
wenshao May 8, 2026
f43f5aa
feat(sdk-python): replace verbatim release notes inheritance with --g…
doudouOUC May 8, 2026
0696f29
ci(release): keep skip-ci out of release PR titles (#3950)
yiliang114 May 8, 2026
bb6ebf0
fix(vscode): mark Qwen OAuth coder-model as Discontinued in model pic…
dreamWB May 8, 2026
4b9377c
chore: Add bilingual requirement to create-issue command (#3952)
pomelo-nwu May 8, 2026
9022aa0
feat(cli): add Idealab as third-party provider (#3955)
pomelo-nwu May 8, 2026
9bd9d5d
feat(session): add /branch to fork the current conversation (#3539)
qqqys May 8, 2026
1fc4b87
feat(core): foreground → background promote integration (#3831 PR-2 o…
wenshao May 8, 2026
4be5a58
fix(cli): show tool details in subagent approval banner (#3956)
tanzhenxin May 8, 2026
60157ec
fix(cli): persist ACP model selection (#3947)
DragonnZhang May 8, 2026
4a817fe
fix(cli): trim blank streaming tails from live preview (#3965)
chiga0 May 8, 2026
cc71b1b
fix(memory): route auto-memory recall selector to fast model
B-A-M-N May 5, 2026
f1f751f
fix(cli): setValueFullSave must write full file to delete keys; fix(p…
B-A-M-N May 8, 2026
3579586
fix(cli): address PR #3937 review feedback
B-A-M-N May 8, 2026
112dde5
fix(test): move stream retry describe block out of createRequestConte…
B-A-M-N May 8, 2026
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
40 changes: 20 additions & 20 deletions .github/workflows/release-sdk-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -373,12 +373,6 @@ jobs:
set -euo pipefail
TAG_NAME="sdk-python-${RELEASE_TAG}"

if [[ "${IS_NIGHTLY}" == "true" || "${IS_PREVIEW}" == "true" ]]; then
PRERELEASE_FLAG="--prerelease"
else
PRERELEASE_FLAG=""
fi

if gh release view "${TAG_NAME}" --json tagName >/dev/null 2>&1; then
echo "::warning::GitHub release ${TAG_NAME} already exists; skipping create."
exit 0
Expand All @@ -403,29 +397,35 @@ jobs:
echo ""
} > "${NOTES_FILE}"

GH_RELEASE_ARGS=()
if [[ -n "${PREVIOUS_RELEASE_TAG}" ]]; then
PREVIOUS_NOTES=$(gh release view "sdk-python-${PREVIOUS_RELEASE_TAG}" --json body -q '.body' 2>&1) || {
ERR_MSG="${PREVIOUS_NOTES}"
case "${ERR_MSG}" in
*"release not found"*|*"Not Found"*|*"HTTP 404"*)
PREVIOUS_NOTES='See commit history for changes.'
;;
*)
echo "::warning::Failed to fetch previous release notes: ${ERR_MSG}"
PREVIOUS_NOTES='See commit history for changes.'
;;
esac
}
printf '%s\n' "${PREVIOUS_NOTES}" >> "${NOTES_FILE}"
PREVIOUS_TAG_NAME="sdk-python-${PREVIOUS_RELEASE_TAG}"
# Verify the previous tag exists in Git before using --notes-start-tag.
# If a prior release published to PyPI but failed to create a GitHub
# release/tag, the tag won't exist — fall back to static notes to
# avoid failing gh release create after PyPI publish.
if git rev-parse "${PREVIOUS_TAG_NAME}" >/dev/null 2>&1; then
GH_RELEASE_ARGS+=(--generate-notes --notes-start-tag "${PREVIOUS_TAG_NAME}")
else
echo "::warning::Previous tag ${PREVIOUS_TAG_NAME} not found; skipping --generate-notes."
echo "See commit history for changes." >> "${NOTES_FILE}"
fi
else
# PREVIOUS_RELEASE_TAG is empty for preview/nightly (not computed)
# and for the very first stable release (no prior stable on PyPI).
# Skip --generate-notes to avoid including non-SDK commits.
echo "See commit history for changes." >> "${NOTES_FILE}"
fi

if [[ "${IS_NIGHTLY}" == "true" || "${IS_PREVIEW}" == "true" ]]; then
GH_RELEASE_ARGS+=(--prerelease)
fi

gh release create "${TAG_NAME}" \
--target "${RELEASE_TARGET_SHA}" \
--title "SDK Python Release ${RELEASE_TAG}" \
--notes-file "${NOTES_FILE}" \
${PRERELEASE_FLAG}
"${GH_RELEASE_ARGS[@]}"

rm -f "${NOTES_FILE}"

Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/release-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ jobs:
pr_url="$(gh pr create \
--base main \
--head "${RELEASE_BRANCH}" \
--title "chore(release): sdk-typescript ${RELEASE_TAG} [skip ci]" \
--title "chore(release): sdk-typescript ${RELEASE_TAG}" \
--body "Automated release PR for sdk-typescript ${RELEASE_TAG}.")"
fi

Expand All @@ -414,10 +414,9 @@ jobs:
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
run: |-
set -euo pipefail
# Keep [skip ci] on the squash commit that lands on main. The release
# PR title also includes it for visibility, but --subject makes the
# post-merge CI-skip behavior explicit instead of depending on gh's
# default squash subject.
# Keep [skip ci] only on the squash commit that lands on main. The
# release branch commit and PR title intentionally omit it so tag-push
# workflows and PR metadata stay unaffected.
gh pr merge "${PR_URL}" \
--squash \
--auto \
Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ jobs:
pr_url="$(gh pr create \
--base main \
--head "${RELEASE_BRANCH}" \
--title "chore(release): ${RELEASE_TAG} [skip ci]" \
--title "chore(release): ${RELEASE_TAG}" \
--body "Automated release PR for ${RELEASE_TAG}. Syncs package.json versions on main.")"
fi

Expand All @@ -450,10 +450,9 @@ jobs:
RELEASE_TAG: '${{ needs.prepare.outputs.release_tag }}'
run: |-
set -euo pipefail
# Keep [skip ci] on the squash commit that lands on main. The release
# PR title also includes it for visibility, but --subject makes the
# post-merge CI-skip behavior explicit instead of depending on gh's
# default squash subject.
# Keep [skip ci] only on the squash commit that lands on main. The
# release branch commit and PR title intentionally omit it so tag-push
# workflows and PR metadata stay unaffected.
gh pr merge "${PR_URL}" \
--squash \
--auto \
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,6 @@ storybook-static

# Dev symlink: qc-helper bundled skill docs (created by scripts/dev.js)
packages/core/src/skills/bundled/qc-helper/docs
tmp/
tmp/.prforge/
.prforge-run
.prforge-*
10 changes: 10 additions & 0 deletions .qwen/commands/qc/create-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ The user provides a brief description of a feature request or bug report:
- Bug report: follow @.github/ISSUE_TEMPLATE/bug_report.yml
- Write from the user's perspective, not as an implementation spec
- Keep the language clear and concise, AVOID internal implementation details
- **Bilingual requirement**: The issue body must be in both English and Chinese
- English content comes first at the top
- Chinese translation goes at the end, wrapped in a `<details>` collapsible tag:
```markdown
<details>
<summary>中文</summary>
(Chinese translation here)
</details>
```
- The issue title stays in English only — do NOT translate the title

4. **Review with user**

Expand Down
111 changes: 111 additions & 0 deletions docs/design/auth/motivation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Auth Provider Registry Motivation

The auth module used to model each setup path as a separate flow: API key,
OAuth, subscription plans, and custom providers. In practice, all of these paths
produce the same kind of output: updates to the user's provider configuration in
`~/.qwen/settings.json`.

This refactor makes provider setup the shared abstraction. A provider describes
how it is shown, how credentials are collected, which models it installs, and
which settings patch should be applied. API keys, OAuth, coding plans, token
plans, and custom wizards are setup methods for a provider, not separate auth
architectures.

## Goals

- Keep `/auth` user-facing flows easy to understand:
- Alibaba ModelStudio for first-party Qwen setup.
- Third-party providers for common built-in integrations such as DeepSeek,
MiniMax, and Z.AI.
- OAuth providers such as OpenRouter.
- Custom providers for local servers, proxies, or providers that are not built
in.
- Move provider-specific data into small declarative provider configs.
- Make third-party provider contributions simple: adding a common provider
should usually mean adding one provider config plus tests.
- Centralize settings writes through `ProviderInstallPlan` and
`applyProviderInstallPlan`.
- Keep UI grouping separate from install behavior. Groups help users navigate
`/auth`; they should not drive settings logic.
- Preserve a path for model list ownership and provider metadata so provider
model updates can be detected and applied safely.

## Architecture

The new structure separates provider definitions, install logic, and UI state:

```text
packages/cli/src/auth/
├── allProviders.ts
├── providerConfig.ts
├── types.ts
├── install/
│ └── applyProviderInstallPlan.ts
└── providers/
├── alibaba/
├── custom/
├── oauth/
└── thirdParty/
```

`ProviderConfig` is the declarative contract for built-in providers. It contains
the provider label, protocol, base URL options, environment key, model list,
model metadata, UI grouping, and setup behavior.

`buildInstallPlan` converts a provider config and collected setup inputs into a
`ProviderInstallPlan`. The install plan is the only object the settings writer
needs to understand.

`applyProviderInstallPlan` applies that plan by updating environment settings,
`modelProviders`, selected auth type, optional model selection, and provider
metadata. This keeps settings persistence independent from the UI flow that
collected the inputs.

## User flows

`/auth` can still present different entry points, but they should all converge on
the same provider install path:

1. **Alibaba ModelStudio**
- Coding Plan
- Token Plan
- Standard API key

2. **Third-party Providers**
- Common providers with built-in defaults.
- Each provider should own its base URL, env key, default models, and model
metadata.
- Z.AI must use the setup-specific base URL:
- Coding Plan: `https://api.z.ai/api/coding/paas/v4`
- Standard API key: `https://api.z.ai/api/paas/v4`

3. **OAuth**
- Browser-based authorization for routing platforms such as OpenRouter.
- OAuth-specific mechanics can live in the provider implementation, but the
final result should still be a provider install plan.

4. **Custom Provider**
- Manual setup for local servers, proxies, or unsupported providers.
- The wizard collects protocol, base URL, API key, model IDs, and advanced
model options such as thinking, multimodal input, context window, and max
tokens.

## Model ownership and updates

Static built-in providers can persist provider metadata under
`providerMetadata.<providerId>`, including the model list version and base URL.
This lets Qwen Code detect when a provider's built-in model list changes and
prompt the user to update owned models without overwriting unrelated custom
models.

Custom providers are different: their model list is user-authored and should not
be treated as an auto-updatable built-in model list.

## Non-goals

- Do not make API key, OAuth, coding plan, or token plan the top-level settings
architecture.
- Do not couple settings writes to React components or CLI command handlers.
- Do not make UI groups a business-logic axis.
- Do not require contributors to understand the full auth UI to add a simple
third-party provider.
4 changes: 2 additions & 2 deletions docs/users/configuration/model-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Use `modelProviders` to declare curated model lists per auth type that the `/mod
>
> Only the `/model` command exposes non-default auth types. Anthropic, Gemini, etc., must be defined via `modelProviders`. The `/auth` command lists Qwen OAuth, Alibaba Cloud Coding Plan, and API Key as the built-in authentication options.

> [!warning]
> [!note]
>
> **Duplicate model IDs within the same authType:** Defining multiple models with the same `id` under a single `authType` (e.g., two entries with `"id": "gpt-4o"` in `openai`) is currently not supported. If duplicates exist, **the first occurrence wins** and subsequent duplicates are skipped with a warning. Note that the `id` field is used both as the configuration identifier and as the actual model name sent to the API, so using unique IDs (e.g., `gpt-4o-creative`, `gpt-4o-balanced`) is not a viable workaround. This is a known limitation that we plan to address in a future release.
> **Model uniqueness:** Models within the same `authType` are uniquely identified by the combination of `id` + `baseUrl`. This means you can define the same model ID (e.g., `"gpt-4o"`) multiple times under a single `authType` as long as each entry has a different `baseUrl` — for example, one pointing to OpenAI directly and another to a proxy endpoint. If two entries share both the same `id` and the same `baseUrl` (or both omit `baseUrl`), the first occurrence wins and subsequent duplicates are skipped with a warning.

## Configuration Examples by Auth Type

Expand Down
11 changes: 7 additions & 4 deletions packages/cli/src/acp-integration/acpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,10 +402,13 @@ class QwenAgent implements Agent {
break;
}
case 'model': {
await this.unstable_setSessionModel({
sessionId,
modelId: value as string,
});
await session.setModel(
{
sessionId,
modelId: value as string,
},
{ persistDefault: false },
);
break;
}
default:
Expand Down
36 changes: 35 additions & 1 deletion packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Session } from './Session.js';
import type { Config, GeminiChat } from '@qwen-code/qwen-code-core';
import { ApprovalMode, AuthType } from '@qwen-code/qwen-code-core';
import * as core from '@qwen-code/qwen-code-core';
import { SettingScope } from '../../config/settings.js';
import type {
AgentSideConnection,
PromptRequest,
Expand Down Expand Up @@ -158,7 +159,11 @@ describe('Session', () => {

mockSettings = {
merged: {},
} as LoadedSettings;
isTrusted: false,
user: { settings: {} },
workspace: { settings: {} },
setValue: vi.fn(),
} as unknown as LoadedSettings;

getAvailableCommandsSpy = vi.mocked(nonInteractiveCliCommands)
.getAvailableCommands as unknown as ReturnType<typeof vi.fn>;
Expand Down Expand Up @@ -216,6 +221,16 @@ describe('Session', () => {
'qwen3-coder-plus',
undefined,
);
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'model.name',
'qwen3-coder-plus',
);
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'security.auth.selectedType',
AuthType.USE_OPENAI,
);
});

it('rejects empty/whitespace model IDs', async () => {
Expand All @@ -227,6 +242,24 @@ describe('Session', () => {
).rejects.toThrow('Invalid params');

expect(mockConfig.switchModel).not.toHaveBeenCalled();
expect(mockSettings.setValue).not.toHaveBeenCalled();
});

it('can switch the session model without persisting a new default', async () => {
await session.setModel(
{
sessionId: 'test-session-id',
modelId: `qwen3-coder-flash(${AuthType.USE_OPENAI})`,
},
{ persistDefault: false },
);

expect(mockConfig.switchModel).toHaveBeenCalledWith(
AuthType.USE_OPENAI,
'qwen3-coder-flash',
undefined,
);
expect(mockSettings.setValue).not.toHaveBeenCalled();
});

it('propagates errors from config.switchModel', async () => {
Expand All @@ -239,6 +272,7 @@ describe('Session', () => {
modelId: `invalid-model(${AuthType.USE_OPENAI})`,
}),
).rejects.toThrow('Invalid model');
expect(mockSettings.setValue).not.toHaveBeenCalled();
});
});

Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/acp-integration/session/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import { isSlashCommand } from '../../ui/utils/commandUtils.js';
import { CommandKind } from '../../ui/commands/types.js';
import { parseAcpModelOption } from '../../utils/acpModelUtils.js';
import { classifyApiError } from '../../ui/hooks/useGeminiStream.js';
import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js';

// Import modular session components
import type {
Expand Down Expand Up @@ -1337,6 +1338,7 @@ export class Session implements SessionContext {
*/
async setModel(
params: SetSessionModelRequest,
options: { persistDefault?: boolean } = {},
): Promise<SetSessionModelResponse | void> {
const rawModelId = params.modelId.trim();

Expand All @@ -1363,6 +1365,16 @@ export class Session implements SessionContext {
? { requireCachedCredentials: true }
: undefined,
);

if (options.persistDefault ?? true) {
const persistScope = getPersistScopeForModelSelection(this.settings);
this.settings.setValue(persistScope, 'model.name', parsed.modelId);
this.settings.setValue(
persistScope,
'security.auth.selectedType',
selectedAuthType,
);
}
}

/**
Expand Down
Loading