Skip to content

feat(import): import link v2 opens the New upstream dialog - #320

Merged
fylorn merged 1 commit into
devfrom
feat/import-v2
Oct 11, 2026
Merged

fylorn merged 1 commit into
devfrom
feat/import-v2

Conversation

@fylorn

@fylorn fylorn commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Implements import link v2 (the contract shared with the website and the enterprise edition, including clarifications v2.1 and v2.2).

What a link can do now

A link can set everything the New upstream dialog lets a user set for the chosen service and authentication method: service, auth, site, name, url, protocol, key, the OAuth credentials, Bedrock region and access keys, headers, client_identity, concurrency, balance (Custom only), models, models_only, specs, billing, price_multiplier and prices.

It opens the New upstream dialog itself, prefilled, instead of a separate confirmation dialog. The v1 ImportDialog and its strings and test are deleted.

Parser (src-tauri/src/import_link.rs)

  • Per-service rules. A parameter is accepted only when the dialog shows its field. A field the dialog fills in by itself (a vendor's address, a fixed protocol, the site's address) must match what the dialog would fill in. The rules are one table, SERVICES.
  • Keeping Rust and TypeScript in sync. tests/ts_bindings.rs exports the table to src/generated/import-services.json (regenerate with UPDATE_TS=1, the same as the bindings). src/import/services.test.ts checks that JSON against PRESETS, fieldRules, authOptions, serviceOf and ZAI_ENDPOINTS. If either side changes without the other, that test fails.
  • No service. The parser takes the service the edit dialog would recognize from the address. If any parameter does not fit that service, the link is read as Custom, so every v1 link stays valid (https://api.openai.com/v1, Ollama with key, DeepSeek with protocol=openai-chat).
  • Unknown parameters produce ImportLink::Unsupported. The UI shows Cannot import this link with the updater's state: Check for updates, or Install update plus the "update, then open this link again" line when an update is known, and Close. The parameter name is shown only when it matches ^[a-z0-9_]{1,32}$. The never-accepted names and invalid values are still rejected silently.
  • Unsafe characters. No value may contain $, {, } or control or invisible characters; for headers the check runs per line.
  • Plain http:// is accepted for loopback, private IPv4/IPv6 ranges and intranet-style names (v2.2). The dialog then adds "This address is not encrypted; the key and request content travel in plain text."
  • take_import_link now returns ImportLink, a proposal or unsupported, through the same one-at-a-time Gate.

Tests cover:

  • each parameter's accept and reject edges;
  • every service × auth combination;
  • each denied header, in both cases, and the prefixes;
  • $, {, } and control or invisible characters in every parameter;
  • length limits;
  • every private-range edge (172.15/172.16, 100.63/100.64, fbff::/fc00::, .corp vs .com, …);
  • v1 links and the site docs' examples;
  • the enterprise link;
  • the gate.

Dialog

  • Opening. The dialog opens in create mode on step 2 (Connection, or Account for auth=account), with step 1 counted as visited. This is the same path as the Bedrock draft (nav.open("upstreams", { create: "upstream", imported })).
  • Host note. Every step shows "From an import link. Requests and keys will be sent to host" (both hosts when the token endpoint is elsewhere). Its height is added to the dialog, so every step that fit before still fits.
  • Nothing is sent early. No host is contacted before Next on step 2 or a sign-in; only the local PreviewProvider and QueryPrice calls run before that. I checked this by reading every effect that runs when the dialog opens, and in the harness, where the calls before Next contain no TestProvider or sign-in and no request leaves for a non-local host.
  • Specs are saved with SetModelSpec after the upstream exists, only for models on the final list. The Models step says which models are skipped. The table shows the link's context window in the meantime.
  • Price sheet. price_multiplier / prices preselect a new sheet named after the upstream. It can be edited as a draft in PriceSheetDialog (new draft mode, which writes nothing) and is created on Create before the upstream. If the upstream cannot be created, the new sheet is deleted again. A taken sheet name blocks Create until it is changed.
  • Account links open on the sign-in panel. A link never replaces the key of an existing Z.ai / BigModel upstream with the same name; the name has to be changed. The link's model and billing values are applied after sign-in.
  • Changing the service. Going back to step 1 and picking another service keeps what that service takes: the address for services without one, an offered protocol, the link's billing. Client identity and balance are dropped where the new service does not take them.
  • The gate. Closing the dialog in any way releases it. A link that arrives while a dialog is open is dropped and the gate released.
  • Balance source (v2.1). Custom upstreams get a Balance source select in Advanced settings. It is shown inline so the expanded section still fits. When it is not Auto-detect, Advanced opens on its own. An upstream that was Custom when the dialog opened keeps the field even after switching to a relay source (that makes the edit dialog recognize it as that relay).

Verification

  • pnpm typecheck and pnpm test (1192 tests) pass.
  • cargo fmt --check, cargo clippy --all-targets -- -D warnings and cargo test pass.
  • Preview harness, 1100×720, zh/en × light/dark. Ten links were opened: the enterprise link, a Sub2API relay with headers, concurrency, models_only, specs and prices, Bedrock with access keys, Custom OAuth with a token endpoint on another host, a ChatGPT account, a Z.ai account with site, a v1 link, an unknown parameter, an intranet http:// gateway, and Custom with a balance source.
    • The dialog body's scrollHeight stays within clientHeight on every step visited.
    • No TestProvider call and no sign-in happens before Next.
    • No request goes to a non-local host.
  • End-to-end flows in the harness:
    • The order of writes on Create: sheet, then upstream, then specs, then the gate is released.
    • A link that arrives while the New upstream form is open is dropped and the form is untouched.
    • Cancel and Esc release the gate, and the next link opens.
    • Taken upstream and sheet names are asked to be changed.
    • Editing the draft sheet writes nothing.
    • The ChatGPT sign-in, then Done, writes the link's models.
    • Cannot-import, with and without a known update.
  • Known exception. Custom + OAuth with Advanced opened was already the one state that did not fit (see 2026.10.15: fit the new-upstream wizard on one screen #319). It does not fit after this change either: 504/442 zh, 507/442 en.

🤖 Generated with Claude Code

Import links could only carry a name, address, protocol, key and model
list, and opened a confirmation dialog where only the name could be
changed. Upstreams have grown since, so v2 (the contract shared with the
website and the enterprise edition) lets a link set everything the New
upstream dialog lets a user set for the chosen service and
authentication method, and opens that dialog itself, prefilled.

Parser (src-tauri/src/import_link.rs):
- A parameter is accepted only when the dialog shows its field for the
  chosen service and auth method; the per-service rules live in one table
  (SERVICES). It is exported to src/generated/import-services.json and
  checked by tests/ts_bindings.rs, and src/import/services.test.ts checks
  that JSON against PRESETS, fieldRules, authOptions and serviceOf, so the
  Rust and TypeScript rules cannot drift apart.
- Without `service` the address picks the service the edit dialog would
  recognize; when a parameter does not fit it, the link is read as Custom,
  so every v1 link stays valid.
- Unknown parameter names are no longer silent: the UI shows "Cannot
  import this link" with the update state (the name only when it matches
  ^[a-z0-9_]{1,32}$). Never-accepted names and invalid values still reject
  the link silently. No value may contain `$`, `{` or `}`.
- Plain http:// is accepted for loopback and private-network addresses
  (v2.2); the dialog then says the address is not encrypted.

Dialog:
- The link opens UpstreamDialog in create mode on step 2, like the Bedrock
  draft path; a one-line note on every step names the host(s) requests
  and keys go to. Nothing reaches any host before Next or a sign-in.
- Specs are written after the upstream exists, only for models on the
  final list; the Models step says which are skipped.
- `price_multiplier` / `prices` preselect a new price sheet, editable as a
  draft and created together with the upstream (removed again if the
  upstream cannot be created). A taken sheet name has to be changed.
- Account links open on the sign-in panel and never replace an existing
  Z.ai upstream's key.
- A link that arrives while a dialog is open is dropped and the gate
  released; closing the import dialog in any way releases it.
- Custom upstreams get a visible Balance source field in Advanced
  settings, so a link's `balance` can be seen and changed.

The note's height is added to the dialog's height, so every step that fit
at 1100x720 before still fits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit bc7300b into dev Oct 11, 2026
5 checks passed
@fylorn
fylorn deleted the feat/import-v2 branch October 11, 2026 13:18
fylorn added a commit that referenced this pull request Oct 11, 2026
* fix(upstreams): undo layout squeezed only to avoid a scrollbar

#319 and #320 made every step of the upstream dialog fit 1100x720 without
a scrollbar, including the steps with many settings. That rule was meant
for steps with few options and a lot of empty space. Steps with many
settings may scroll: the dialog keeps its fixed height and its body
scrolls. Options crammed into columns, descriptions dropped and results
moved into hovers only to save a few rows go back to their natural form.
The approved redesign stays (service tiles and search, auth rows, Advanced
settings collapsed unless set, Check on Next when creating, sign-in cards,
account summary rows, the import host note).

- Edit dialog: Check connection is back in the Connection section body,
  with the full result and the balance below it, not in the footer with
  the details in a hover.
- Advanced settings, expanded: headers full width, then Forward client
  identity with its description, then the short fields two per row
  (proxy unavailable, concurrency limit with its description, and the
  balance source for Custom as a normal labelled field).
- Connection: the base URL is full width again, protocol and region share
  the next row; AWS access keys and OAuth go back to two columns with the
  session token and access token on their own rows; the English
  "Auto-detect (not detected, forwarded as is)" that was shortened for the
  narrow protocol column is back. Spacing back to the form's gap-4.
- Descriptions restored, as worded before: base URL, headers (${NAME}),
  session token, OAuth access token, and the egress note on the ChatGPT
  account page; also the Z.ai key-storage notice and the full device code
  warning, which had been cut to fit one line.
- ChatGPT account page: name and egress in two columns with the
  concurrency limit below, usage limits above reset credits, gap-5.
- Import link: the dialog no longer grows by the note's height; the note
  takes its space from the body, and long host names wrap instead of being
  cut off.
- Billing: the price sheet rows keep their normal padding, and the import
  link's new-sheet row wraps its detail and its error instead of
  truncating them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Base URL and protocol share a row again

The user approved that row in the design; a step with few fields does
not need the extra line, and the base URL hint still sits under the
address. The undetected label stays short for the narrower column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant