Skip to content

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

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

fix(cursor): login URL visibility, model registry, and oauth-model-alias#124
tan-yong-sheng wants to merge 13 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
Contributor

Declaration

This pull request is vibe coded with GLM 5.2 + Kimi K2.7 + Minimax M3 + Deepseek v4 flash 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
Contributor Author

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

…treaming delta

extractFirstToolCall was returning a single map[string]interface{} (the first
tool call object) instead of []interface{} (the full tool_calls array),
causing choices[0].delta.tool_calls to be an object instead of an array
per OpenAI streaming schema.

Both call sites updated: sendChunkSwitchable now wraps extractFirstToolCall
result in map with "tool_calls" key pointing to the full array.
@tan-yong-sheng

Copy link
Copy Markdown
Contributor Author

You may review if this is appropriate to merge or not

@kaitranntt

Copy link
Copy Markdown
Owner

Thanks for the Cursor bundle. The safe, independently validated pieces are now released:

The tool-stream series is not being merged: there is no safe reproducer, and the original discussion identifies unresolved index, resume, DONE, and timeout defects. Closing this combined PR as superseded by the focused replacements.

@kaitranntt kaitranntt closed this Jul 10, 2026
tan-yong-sheng pushed a commit to tan-yong-sheng/CLIProxyAPIPlus that referenced this pull request Jul 10, 2026
Replaces the oauth-model-alias portion of contributor PR kaitranntt#124 (59b83e0) with a focused boundary fix and regression coverage.
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

2 participants