Skip to content

fix(billing): hand off billing to hosted console - #408

Open
lucas77778 wants to merge 7 commits into
masterfrom
chenyu/code-567
Open

fix(billing): hand off billing to hosted console#408
lucas77778 wants to merge 7 commits into
masterfrom
chenyu/code-567

Conversation

@lucas77778

@lucas77778 lucas77778 commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace LinkCode's balance, offers, orders, subscription, portal, custom amount, and checkout implementation with a single hosted billing handoff built by the official @linkcode/cloud SDK.
  • Desktop exposes only openHostedBilling(): main registers the current linkcode:// / linkcode-dev:// scheme, builds https://console.linkcode.ai/billing?returnTarget=..., and opens it in the system browser.
  • Webview builds the credential-free hosted URL through the same SDK and opens it in a new browser tab.
  • Reduce the shared UI contract to BillingSettingsPanel({ onOpenBilling }); LinkCode does not request, store, render, or infer billing/checkout/payment state.
  • Remove the sidebar balance shortcut and all LinkCode billing API sources, providers, IPC channels, and preload methods.

Linear: CODE-567
Cloud counterpart: arcboxlabs/linkcodehq#39 at 5438df8c8083f349dc3b8ed3eccdd61ca9d86358

Contracts

  • Cloud SDK: createHostedBillingUrl(options?: { returnTarget?: string }): string; HostedBillingUrlOptions is exported.
  • Hosted surface: https://console.linkcode.ai/billing[?returnTarget=<encoded>].
  • Desktop return target: ${scheme}://billing/return, where scheme is linkcode in packaged builds and linkcode-dev in development.
  • Cloud's existing native return bridge appends authoritative order=<id> and, on cancellation, canceled=true. Desktop deliberately does not parse either parameter and does not process checkout; the existing generic deep-link path only activates/focuses the app.
  • Until the initial public npm bootstrap of @linkcode/cloud@0.1.0, this repository consumes the official package tarball built from the Cloud commit above, pinned at SHA-256 576bf95543503615204163609c49fe66ad38a3081babf5b93369a94842acdb7b.

Verification

  • NODE_OPTIONS=--max-old-space-size=6144 pnpm check:ci — passed: format, repo-wide ESLint (0 errors; 377 existing warnings), and typecheck.
  • pnpm test — passed: 307 files / 2443 tests; 1 existing skipped file/test.
  • Hosted handoff unit tests — passed for Desktop main, Webview route, and shared Billing panel.
  • pnpm -F @linkcode/desktop run build — passed.
  • pnpm -F @linkcode/webview run build — passed.
  • pnpm -F @linkcode/webview e2e:browser — passed production billing-route smoke plus bundled mock router/wire/reload smoke.
  • Bundle inspection found the Cloud hosted URL and no checkout-sessions, billing summary/orders, or portal-session endpoints.
  • Built Desktop Electron smoke — opened the Billing settings page, launched a second instance with linkcode-dev://billing/return?order=...- Commit hookscanceled=true, observed one restored/focused window, and observed no local checkout-result UI.
  • Commit hooks — passed large-file policy, formatting/imports, lint, and typecheck.

Checklist

  • pnpm check:ci and pnpm test both pass
  • I ran the affected surface and observed the change working
  • No wire message changed
  • The vendored SDK tarball comes from the official Cloud package/commit documented above
  • Docs and comments are updated where behavior changed

Copilot AI lite review requested due to automatic review settings August 3, 2026 14:58
@linear-code

linear-code Bot commented Aug 3, 2026

Copy link
Copy Markdown

CODE-567

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 04:46

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@lucas77778 lucas77778 changed the title feat(billing): add LinkCode billing surface fix(billing): hand off billing to hosted console Aug 4, 2026
@lucas77778
lucas77778 requested review from AprilNEA and Zerlight and a lite review from Copilot August 4, 2026 07:15

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 5, 2026 02:11

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@lucas77778
lucas77778 requested a review from PeronGH August 5, 2026 05:00

@PeronGH PeronGH left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two correctness issues on the typed gateway-billing path; details inline.

Comment on lines +1391 to +1400
if (
this.emitGatewayError({
statusCode: msg.api_error_status,
message: msg.result,
})
) {
this.teardown();
if (!cancelling) this.emitStatus('idle');
return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can never fire, so claude-code never gets a typed billing error.

The LinkCode Gateway account is protocol: 'openai-chat' (catalog.ts), which is exactly the combination translationUpstream() claims: packages/host/engine/src/agent/translator.ts:35-37 matches kind === 'claude-code' + config.protocol === 'openai-chat', and SessionStartOptionsResolver.resolve (session/start-options-resolver.ts:46-63) then runs withTranslatorEndpoint, which overwrites config.baseUrl with the loopback sidecar URL and sets protocol: 'anthropic'. By the time start() stores opts, this.opts.config.baseUrl is http://127.0.0.1:PORT, so gatewayHost() in linkCodeGatewayError is always false here — this block, and the emitProviderError on the non-success path below, are dead for gateway sessions. Codex/opencode/pi are unaffected because translationUpstream returns undefined for them and their baseUrl stays the gateway host.

Net effect: on the flagship agent a gateway 402 surfaces as raw error text with no top-up button. The upstream URL is spread away by withTranslatorEndpoint, so nothing downstream can recover it — the fix belongs in the resolver (carry the pre-rewrite upstream in config, e.g. upstreamBaseUrl, and have linkCodeGatewayError consult it). Worth confirming first that the arcboxlabs/aigateway sidecar even forwards the upstream 402 status/body, otherwise the typed error can't be reconstructed on this path at all.

Separately, and regardless of how the routing is resolved: msg.result is the final assistant text on subtype === 'success', and typedCode substring-matches it for insufficient_credits. Once host detection starts working here, a reply that merely mentions the token converts a successful turn into an error and returns before emitUsage/emitStop, losing the turn's usage and stop event. Only statusCode is trustworthy on the success path — drop message: msg.result from this call.

@PeronGH PeronGH left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Correction to my earlier 503 comment (retracted inline — the mapping is contract-backed), and the real problem the Cloud contract exposes instead.

insufficientCreditsHint: 'Top up your balance, then retry this message.',
topUpCredits: 'Top up credits',
billingUnavailableTitle: 'LinkCode billing is temporarily unavailable',
billingUnavailableHint: 'Your request was not sent to the model. Please retry shortly.',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This contradicts Cloud's documented 503 semantics. apps/web/content/docs/reference/ai-api-errors.mdx in linkcodehq says of 503 billing_unavailable:

This error can occur either before an upstream call or after a provider response while the charge is being persisted, so do not assume that no completion was generated.

The gateway backs that up: chat-completions.ts:490 returns billing_unavailable on the post-response persistence failure, after the upstream completion already happened (and was possibly already streamed). Telling the user "Your request was not sent to the model" is therefore wrong in that case, and it invites a retry that duplicates a completion the org may already owe for.

Something like "Billing could not record this request — it may or may not have reached the model. Retry shortly." keeps the retry advice without the false guarantee.

insufficientCreditsHint two lines up needs no change: the same doc guarantees 402 is generated before any upstream provider call and that retrying after a top-up is safe.

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.

3 participants