Skip to content

fix(cursor): login URL visibility, model registry, and oauth-model-alias#124

Open
tan-yong-sheng wants to merge 4 commits into
kaitranntt:mainfrom
tan-yong-sheng:fix/cursor-bundle
Open

fix(cursor): login URL visibility, model registry, and oauth-model-alias#124
tan-yong-sheng wants to merge 4 commits into
kaitranntt:mainfrom
tan-yong-sheng:fix/cursor-bundle

Conversation

@tan-yong-sheng

@tan-yong-sheng tan-yong-sheng commented Jun 27, 2026

Copy link
Copy Markdown

Declaration

This pull request is vibe coded with GLM 5.2 + Minimax M3 and Pi Coding Agent.

Summary

Three independent cursor fixes discovered while debugging issue #122:

  1. --cursor-login prints the URL on stdout even with logging-to-file: true
  2. Model registry no longer collapses to stale fallback on transient GetUsableModels failures
  3. oauth-model-alias now works for cursor — the alias resolves correctly to the upstream model name

Each fix has its own commit in this PR for independent review and revert.


Fix 1 — Login URL visibility (issue #122)

When logging-to-file: true, ConfigureLogOutput redirects all logrus output to ~/.cli-proxy-api/logs/main.log. The cursor login URL was therefore hidden in the log file and the program appeared to hang with a blinking cursor on the terminal — no indication that the URL was waiting in the log.

Switched the four user-facing prompts in sdk/auth/cursor.go from log.* to fmt.Println/Printf so they always reach stdout regardless of the logging setting. log.Warnf for the browser-open failure stays on logrus (diagnostic, not user-facing).

Verified end-to-end: with the new binary and logging-to-file: true, the terminal prints the URL immediately.

Fixes #122


Fix 2 — Stale model registry on transient failure

FetchCursorModels previously returned GetCursorFallbackModels() on every failure (DNS timeout, non-2xx, empty parse, etc.). The hardcoded list was also months out of date — it contained composer-2, claude-3.5-sonnet, gpt-4o, cursor-small, gemini-2.5-pro — none of which Cursor serves today.

Combined effect: a single transient GetUsableModels failure (observed in the production log at 2026-06-27 21:53:33, dns i/o timeout to api2.cursor.sh) caused the registry reconcile to overwrite the live 119-model list with the stale 6-model fallback, permanently removing composer-2.5 and friends from the registry until the auth file was reloaded. User calls to cursor/composer-2.5 then returned an instant 503 with no executor trace.

Two changes:

  • Last-good cache. Package-level cursorModelsCache keyed by auth.ID. On successful live fetch, store the result. On failure, return the cached list when available; fall back to GetCursorFallbackModels only when no prior fetch has populated the cache. A transient blip can no longer collapse the registry.
  • Refresh GetCursorFallbackModels to current Cursor model ids (composer-2.5, composer-2.5-fast, gpt-5.3-codex, gpt-5.2, gpt-5.5-high, gpt-5.4-high, claude-opus-4-8-thinking-high, claude-opus-4-7-thinking-high, claude-4.5-sonnet, gemini-3.1-pro). Verified live against api2.cursor.sh on 2026-06-27.

Verified: with the new binary, a configured cursor auth registers 119 models from the live fetch, and a subsequent network blip leaves the registry intact.


Fix 3 — oauth-model-alias resolution for cursor

The cursor executor used parsed.Model (the model name extracted from the translated request payload) when building the upstream Run request to api2.cursor.sh. For OpenAI-source requests the translator is skipped entirely (the from != "openai" guard), so parsed.Model always equalled whatever the client sent — including an alias name like cursor/composer-2.5 defined under oauth-model-alias in config.yaml.

The conductor does resolve the alias correctly: applyOAuthModelAlias in sdk/cliproxy/auth/conductor.go sets the upstream model name on req.Model. But because parsed.Model still held the alias, the protobuf Run request was encoded with the alias as ModelId. Cursor's Run endpoint does not know cursor/composer-2.5 (only composer-2.5) and returned Connect error not_found. Calling the upstream model name directly worked because no alias was involved.

Fix: buildRunRequestParams gets a third modelOverride parameter. When non-empty it is used as ModelId; otherwise parsed.Model is used so existing behavior is unchanged. All four call sites pass req.Model. parsed.Model is untouched, so the OpenAI response still echoes the client-facing alias name.

Verified: with config:

oauth-model-alias:
  cursor:
    - name: composer-2.5
      alias: cursor/composer-2.5

both cursor/composer-2.5 and composer-2.5 return 200; the alias call's response has "model":"cursor/composer-2.5" (alias echoed), the real-name call has "model":"composer-2.5" (real name). Both reach Cursor with model=composer-2.5.


Test plan

  • gofmt -l clean
  • go build ./cmd/server ok
  • go test ./... — 0 FAILs across the repo
  • New tests: TestCursorModelsOrFallback_PrefersCacheOverHardcoded, TestGetCursorFallbackModels_IsCurrent, TestBuildRunRequestParams_ModelOverride (4 subcases)
  • End-to-end with new binary: cursor login URL on stdout, live model registration, alias call success

Files changed

File Change
sdk/auth/cursor.go +10/-5
internal/runtime/executor/cursor_executor.go +99/-20
internal/runtime/executor/cursor_models_cache_test.go new (~90 lines)
internal/runtime/executor/cursor_executor_buildrequest_test.go new (~50 lines)

When logging-to-file is true, ConfigureLogOutput redirects all logrus
output to ~/.cli-proxy-api/logs/main.log. The cursor login URL was
therefore hidden in the log file and the program appeared to hang
with no output on the terminal.

Switch the four user-facing prompts (Starting / URL / Waiting /
Success) in sdk/auth/cursor.go from log.* to fmt.Println/Printf so
they always reach stdout, regardless of the logging-to-file setting.
log.Warnf for the browser-open failure is kept on logrus since it is
diagnostic, not user-facing.

Fixes kaitranntt#122
FetchCursorModels previously returned GetCursorFallbackModels() on every
failure (DNS timeout, non-2xx, empty parse, etc.). The hardcoded list was
also months out of date — it contained composer-2, claude-3.5-sonnet,
gpt-4o, cursor-small, gemini-2.5-pro — none of which Cursor serves today.

Combined effect: a single transient GetUsableModels failure (observed at
2026-06-27 21:53:33, dns i/o timeout to api2.cursor.sh) caused the
registry reconcile to overwrite the live 119-model list with the stale
6-model fallback, permanently removing composer-2.5 and friends from
the registry until the auth file was reloaded. User calls to
cursor/composer-2.5 then returned an instant 503 with no executor trace.

Two changes:

1. Add a package-level cache keyed by auth.ID. On a successful live
   fetch, store the result. On failure, return the cached list when
   available; fall back to GetCursorFallbackModels only when no prior
   fetch has populated the cache. A transient blip can no longer
   collapse the registry.

2. Refresh GetCursorFallbackModels to current Cursor model ids
   (composer-2.5, composer-2.5-fast, gpt-5.3-codex, gpt-5.2,
   gpt-5.5-high, gpt-5.4-high, claude-opus-4-8-thinking-high,
   claude-opus-4-7-thinking-high, claude-4.5-sonnet, gemini-3.1-pro).
   Verified live against api2.cursor.sh on 2026-06-27.

Adds cursor_models_cache_test.go covering both the cache-or-fallback
preference and the freshness of the hardcoded list.
The cursor executor used parsed.Model (the model name extracted from the
translated request payload) when building the upstream Run request to
api2.cursor.sh. For OpenAI-source requests the translator is skipped
entirely (the from!=openai guard at line 293), so parsed.Model always
equalled whatever the client sent — including an alias name like
"cursor/composer-2.5" defined under oauth-model-alias in config.yaml.

The conductor does resolve the alias correctly: applyOAuthModelAlias in
sdk/cliproxy/auth/conductor.go sets the upstream model name on
req.Model. But because parsed.Model still held the alias, the protobuf
Run request was encoded with the alias as ModelId. Cursor's Run
endpoint does not know "cursor/composer-2.5" (only "composer-2.5")
and returned Connect error not_found. Calling the upstream model name
directly worked because no alias was involved and the payload already
held the real name.

Fix: give buildRunRequestParams a third modelOverride parameter. When
non-empty it is used as ModelId; otherwise parsed.Model is used so
existing call sites behave unchanged. All four call sites (Execute +
three sites in ExecuteStream's checkpoint branches) pass req.Model.
parsed.Model is untouched, so the OpenAI response still echoes the
client-facing alias name as before.

Adds cursor_executor_buildrequest_test.go covering override wins over
parsed.Model, empty override falls back, whitespace override falls
back, and override wins even when parsed.Model is empty.
Addresses three findings from PR #1 review (Gemini Code Assist):

1. FetchCursorModels dereferenced auth.ID before any nil check, so a
   nil auth (legal per the function signature; caller in service.go
   currently passes a non-nil auth but the signature allows nil)
   would panic at line 1536 and crash the registry reconcile
   goroutine. Add an explicit nil guard at function entry that
   returns the hardcoded fallback.

2. cursorModelsOrFallback returned the cached slice directly. If a
   caller replaced a slice element via the returned slice (e.g.
   'got[0] = newEntry'), the change propagated to the cache and
   corrupted future failure-path fetches. Return a shallow copy
   instead. ModelInfo pointer fields (e.g. *ThinkingSupport) are
   still shared -- callers must treat returned models as immutable,
   which is the existing convention used by registry reconcile.

3. cacheCursorModels stored the caller's slice directly. If the
   caller mutated its own slice header (append, element replace)
   after caching, the cache was corrupted. Store a shallow copy.

Adds three regression tests covering each finding:

- TestFetchCursorModels_NilAuthReturnsFallback
- TestCursorModelsOrFallback_ReturnIsDefensivelyCopied
- TestCacheCursorModels_StoresDefensiveCopy

Verified go test -race on the executor package.
@tan-yong-sheng

Copy link
Copy Markdown
Author

Don't merge, not working when tool calling ...

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.

./CLIProxyAPIPlus --cursor-login --no-browser not working

1 participant