Skip to content

Commit 1231e9a

Browse files
lidge-junWibias
andcommitted
fix(providers): rename qwen-portal to Qwen Cloud + fix baseUrl (#185)
Renames preset qwen-portal to qwen-cloud / Qwen Cloud. Default baseUrl is pay-as-you-go: https://dashscope.aliyuncs.com/compatible-mode/v1. Registry documents token plan endpoint. Docs, icon map, and parity tests updated. Squash-merged from Wibias:fix/qwen-cloud-preset (original head). Co-Authored-By: Wibias <37517432+Wibias@users.noreply.github.com>
1 parent 30a71fe commit 1231e9a

21 files changed

Lines changed: 355 additions & 28 deletions

File tree

README.ko.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ opencodex는 두 가지 동작을 분리해서 유지합니다:
232232
| Ollama / vLLM / LM Studio (로컬) | `openai-chat` | key (보통 비워둠) |
233233
| 모든 OpenAI 호환 엔드포인트 | `openai-chat` | key |
234234

235-
그 외에 DeepSeek, Groq, OpenRouter, Together, Fireworks, Cerebras, Mistral, Hugging Face, NVIDIA NIM, MiniMax, Qwen Portal 등이 있습니다. 전체 목록은 `ocx init` 또는 [프로바이더 문서](https://lidge-jun.github.io/opencodex/ko/reference/configuration/)에서 확인하세요.
235+
그 외에 DeepSeek, Groq, OpenRouter, Together, Fireworks, Cerebras, Mistral, Hugging Face, NVIDIA NIM, MiniMax, Qwen Cloud 등이 있습니다. 전체 목록은 `ocx init` 또는 [프로바이더 문서](https://lidge-jun.github.io/opencodex/ko/reference/configuration/)에서 확인하세요.
236236

237237
## CLI
238238

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ next Codex session. opencodex keeps these behaviors:
254254
| Ollama / vLLM / LM Studio (local) | `openai-chat` | key (usually blank) |
255255
| Any OpenAI-compatible endpoint | `openai-chat` | key |
256256

257-
Plus DeepSeek, Groq, OpenRouter, Together, Fireworks, Cerebras, Mistral, Hugging Face, NVIDIA NIM, MiniMax, Qwen Portal, and more. See the full list with `ocx init` or in the [provider docs](https://lidge-jun.github.io/opencodex/reference/configuration/).
257+
Plus DeepSeek, Groq, OpenRouter, Together, Fireworks, Cerebras, Mistral, Hugging Face, NVIDIA NIM, MiniMax, Qwen Cloud, and more. See the full list with `ocx init` or in the [provider docs](https://lidge-jun.github.io/opencodex/reference/configuration/).
258258

259259
Cursor support is a staged experimental bridge: it appears in `ocx init` and the dashboard Add
260260
Provider picker as a local config with Cursor's static public model catalog. Live

README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ opencodex 保持两种独立行为:
225225
| Ollama / vLLM / LM Studio(本地) | `openai-chat` | key(通常留空) |
226226
| 任意 OpenAI 兼容端点 | `openai-chat` | key |
227227

228-
此外还有 DeepSeek、Groq、OpenRouter、Together、Fireworks、Cerebras、Mistral、Hugging Face、NVIDIA NIM、MiniMax、Qwen Portal 等等。完整列表可通过 `ocx init` 查看,或参阅 [provider 文档](https://lidge-jun.github.io/opencodex/zh-cn/reference/configuration/)
228+
此外还有 DeepSeek、Groq、OpenRouter、Together、Fireworks、Cerebras、Mistral、Hugging Face、NVIDIA NIM、MiniMax、Qwen Cloud 等等。完整列表可通过 `ocx init` 查看,或参阅 [provider 文档](https://lidge-jun.github.io/opencodex/zh-cn/reference/configuration/)
229229

230230
## CLI
231231

docs-site/src/content/docs/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ validates the key, and stores it. Notable entries:
120120
| Hugging Face | `https://router.huggingface.co/v1` |
121121
| NVIDIA NIM | `https://integrate.api.nvidia.com/v1` |
122122
| Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` |
123-
| Qwen Portal | `https://portal.qwen.ai/v1` |
123+
| Qwen Cloud | Token plan (default): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · Pay as you go: `https://dashscope.aliyuncs.com/compatible-mode/v1` · or Custom |
124124
| Xiaomi MiMo | `https://api.xiaomimimo.com/anthropic` |
125125
| Kilo | `https://api.kilo.ai/api/gateway` |
126126
| GitLab Duo | `https://cloud.gitlab.com/ai/v1/proxy/openai/v1` |

docs-site/src/content/docs/ko/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ opencodex v2.7.1에는 빌트인 프리셋이 50개 들어 있습니다. 키 방
117117
| Hugging Face | `https://router.huggingface.co/v1` |
118118
| NVIDIA NIM | `https://integrate.api.nvidia.com/v1` |
119119
| Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` |
120-
| Qwen Portal | `https://portal.qwen.ai/v1` |
120+
| Qwen Cloud | Token plan(기본): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · 종량제: `https://dashscope.aliyuncs.com/compatible-mode/v1` · 또는 사용자 지정 |
121121
| Xiaomi MiMo | `https://api.xiaomimimo.com/anthropic` |
122122
| Kilo | `https://api.kilo.ai/api/gateway` |
123123
| GitHub Copilot · GitLab Duo | `https://api.githubcopilot.com` · `https://cloud.gitlab.com/ai/v1/proxy/openai/v1` |

docs-site/src/content/docs/zh-cn/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提
110110
| Hugging Face | `https://router.huggingface.co/v1` |
111111
| NVIDIA NIM | `https://integrate.api.nvidia.com/v1` |
112112
| Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` |
113-
| Qwen Portal | `https://portal.qwen.ai/v1` |
113+
| Qwen Cloud | Token plan(默认): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · 按量付费: `https://dashscope.aliyuncs.com/compatible-mode/v1` · 或自定义 |
114114
| Xiaomi MiMo | `https://api.xiaomimimo.com/anthropic` |
115115
| Kilo | `https://api.kilo.ai/api/gateway` |
116116
| GitHub Copilot · GitLab Duo | `https://api.githubcopilot.com` · `https://cloud.gitlab.com/ai/v1/proxy/openai/v1` |

gui/src/base-url-choice.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Resolve a baseUrlChoices selection to the form baseUrl value.
3+
* Known presets write their fixed URL; custom keeps the previous/custom text.
4+
*/
5+
export type BaseUrlChoice = { id: string; label: string; baseUrl?: string };
6+
7+
export function baseUrlForChoice(
8+
choices: BaseUrlChoice[] | undefined,
9+
choiceId: string,
10+
previousBaseUrl: string,
11+
): string {
12+
const choice = choices?.find(c => c.id === choiceId);
13+
if (!choice) return previousBaseUrl;
14+
if (choice.baseUrl) return choice.baseUrl;
15+
// Switching into custom: clear known preset URLs so the field is empty for paste.
16+
const known = new Set(
17+
(choices ?? [])
18+
.map(c => c.baseUrl?.trim().replace(/\/+$/, ""))
19+
.filter((u): u is string => !!u),
20+
);
21+
const prev = previousBaseUrl.trim().replace(/\/+$/, "");
22+
if (known.has(prev)) return "";
23+
return previousBaseUrl;
24+
}
25+
26+
export function matchChoiceId(
27+
choices: BaseUrlChoice[] | undefined,
28+
baseUrl: string,
29+
): string {
30+
if (!choices?.length) return "custom";
31+
const normalized = baseUrl.trim().replace(/\/+$/, "");
32+
for (const choice of choices) {
33+
if (!choice.baseUrl) continue;
34+
if (choice.baseUrl.trim().replace(/\/+$/, "") === normalized) return choice.id;
35+
}
36+
return choices.some(c => c.id === "custom") ? "custom" : choices[0]!.id;
37+
}
38+
39+
/** Resolve the URL that should be persisted for the current dropdown selection. */
40+
export function resolvedBaseUrlForChoice(
41+
choices: BaseUrlChoice[] | undefined,
42+
choiceId: string,
43+
customBaseUrl: string,
44+
): string {
45+
const choice = choices?.find(c => c.id === choiceId);
46+
if (choice?.baseUrl) return choice.baseUrl.trim();
47+
return customBaseUrl.trim();
48+
}

gui/src/components/AddProviderModal.tsx

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "../provider-payload";
1111
import ProviderCatalog from "./provider-catalog/ProviderCatalog";
1212
import type { CatalogPreset } from "./provider-catalog/provider-presets";
13+
import { baseUrlForChoice, matchChoiceId, resolvedBaseUrlForChoice } from "../base-url-choice";
1314

1415
export type ProviderConfig = ProviderPayload;
1516

@@ -55,6 +56,7 @@ export default function AddProviderModal({
5556
const [presets, setPresets] = useState<Preset[]>(fallbackPresets);
5657
const [presetsLoading, setPresetsLoading] = useState(true);
5758
const [usageRank, setUsageRank] = useState<Record<string, number>>({});
59+
const [endpointChoice, setEndpointChoice] = useState("custom");
5860
const aliveRef = useRef(true);
5961
const loadedPresetsRef = useRef(false);
6062
const previousFocusRef = useRef<HTMLElement | null>(null);
@@ -114,10 +116,14 @@ export default function AddProviderModal({
114116

115117
const choosePreset = (p: Preset) => {
116118
setPreset(p);
119+
const choiceId = matchChoiceId(p.baseUrlChoices, p.baseUrl);
120+
setEndpointChoice(choiceId);
117121
setForm({
118122
name: p.id === "custom" ? "" : p.id,
119123
adapter: p.adapter,
120-
baseUrl: p.baseUrl,
124+
baseUrl: p.baseUrlChoices?.length
125+
? baseUrlForChoice(p.baseUrlChoices, choiceId, p.baseUrl)
126+
: p.baseUrl,
121127
authMode: p.auth,
122128
apiKey: "",
123129
defaultModel: p.defaultModel ?? "",
@@ -134,6 +140,7 @@ export default function AddProviderModal({
134140
const back = () => {
135141
setPreset(null);
136142
setForm(null);
143+
setEndpointChoice("custom");
137144
setError("");
138145
setOauthMsg("");
139146
setOauthMsgTone("ok");
@@ -145,11 +152,15 @@ export default function AddProviderModal({
145152
const submit = async () => {
146153
if (!form) return;
147154
const reserved = preset ? isReservedCodexForwardPreset(preset) : false;
155+
const resolvedBaseUrl = preset?.baseUrlChoices?.length
156+
? resolvedBaseUrlForChoice(preset.baseUrlChoices, endpointChoice, form.baseUrl)
157+
: form.baseUrl.trim();
148158
if (!reserved && !form.name.trim()) { setError(t("modal.nameRequired")); return; }
149-
if (!reserved && !form.baseUrl.trim()) { setError(t("modal.baseUrlRequired")); return; }
159+
if (!reserved && !resolvedBaseUrl) { setError(t("modal.baseUrlRequired")); return; }
160+
const submitForm = { ...form, baseUrl: resolvedBaseUrl };
150161
let postBody: { name: string; provider: ProviderPayload };
151162
try {
152-
postBody = buildProviderPostBody(preset ?? { id: "custom" }, form);
163+
postBody = buildProviderPostBody(preset ?? { id: "custom" }, submitForm);
153164
} catch {
154165
setError(t("modal.invalidPreset"));
155166
return;
@@ -388,9 +399,47 @@ export default function AddProviderModal({
388399
{["openai-responses", "openai-chat", "anthropic", "google", "azure-openai", "cursor"].map(a => <option key={a} value={a}>{a}</option>)}
389400
</select>
390401
</Field>
391-
<Field label={t("modal.baseUrl")}>
392-
<input className="input" value={form.baseUrl} onChange={e => setForm({ ...form, baseUrl: e.target.value })} placeholder={t("modal.baseUrlPlaceholder")} />
393-
</Field>
402+
{preset.baseUrlChoices && preset.baseUrlChoices.length > 0 ? (
403+
<>
404+
<Field label={t("modal.endpoint")}>
405+
<select
406+
className="input"
407+
value={endpointChoice}
408+
onChange={e => {
409+
const id = e.target.value;
410+
setEndpointChoice(id);
411+
setForm({
412+
...form,
413+
baseUrl: baseUrlForChoice(preset.baseUrlChoices, id, form.baseUrl),
414+
});
415+
}}
416+
>
417+
{preset.baseUrlChoices.map(c => (
418+
<option key={c.id} value={c.id}>
419+
{c.id === "token-plan" ? t("modal.endpoint.tokenPlan")
420+
: c.id === "payg" ? t("modal.endpoint.payAsYouGo")
421+
: c.id === "custom" ? t("modal.endpoint.custom")
422+
: c.label}
423+
</option>
424+
))}
425+
</select>
426+
</Field>
427+
{endpointChoice === "custom" && (
428+
<Field label={t("modal.baseUrl")}>
429+
<input
430+
className="input"
431+
value={form.baseUrl}
432+
onChange={e => setForm({ ...form, baseUrl: e.target.value })}
433+
placeholder={t("modal.baseUrlPlaceholder")}
434+
/>
435+
</Field>
436+
)}
437+
</>
438+
) : (
439+
<Field label={t("modal.baseUrl")}>
440+
<input className="input" value={form.baseUrl} onChange={e => setForm({ ...form, baseUrl: e.target.value })} placeholder={t("modal.baseUrlPlaceholder")} />
441+
</Field>
442+
)}
394443
{(isCustom || isLocal) && (
395444
<label className="modal-field" style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
396445
<input type="checkbox" checked={form?.allowPrivateNetwork ?? false} onChange={e => setForm(f => f ? { ...f, allowPrivateNetwork: e.target.checked } : f)} />

gui/src/components/provider-catalog/provider-presets.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export interface CatalogPreset {
2727
keyOptional?: boolean;
2828
/** Free pricing — may still require an API key (e.g. NVIDIA NIM). */
2929
freeTier?: boolean;
30+
/**
31+
* Endpoint picker (e.g. Qwen Cloud). Choice without `baseUrl` = Custom (show text field).
32+
*/
33+
baseUrlChoices?: Array<{ id: string; label: string; baseUrl?: string }>;
3034
codexAccountMode?: "direct" | "pool";
3135
provider?: ProviderPayload;
3236
}

gui/src/components/provider-workspace/ProviderDetails.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,9 @@ export default function ProviderDetails({
217217
)}
218218
{tab === "settings" && (
219219
<ProviderSettings
220+
key={item.name}
220221
item={item}
222+
apiBase={apiBase}
221223
availableModels={availableModels}
222224
onUpdateProvider={onUpdateProvider}
223225
onDirtyChange={setSettingsDirty}

0 commit comments

Comments
 (0)