Skip to content

fix(claude): skip auto-complete when no Anthropic credential resolves (#425) - #426

Open
pjdoland wants to merge 3 commits into
plmbr:mainfrom
pjdoland:fix/425-inline-completion-missing-key
Open

pjdoland wants to merge 3 commits into
plmbr:mainfrom
pjdoland:fix/425-inline-completion-missing-key

Conversation

@pjdoland

@pjdoland pjdoland commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

In Claude mode, a user whose Claude CLI holds a subscription login has no Anthropic API key for NBI to find, and auto-complete crashed on every pause in typing. Claude mode's chat goes through the CLI, but inline completions call the Anthropic API directly, and the Anthropic SDK raises only when it builds a request, so the model was constructed successfully and then failed on every call:

TypeError: "Could not resolve authentication method. Expected one of api_key,
auth_token, or credentials to be set..."

Nothing degraded gracefully: each debounced keystroke pause produced a fresh traceback in the server log. The reporter's log shows four in 70 seconds, and the default settings reach this state with no explicit user choice, because the Claude auto-complete dropdown defaults to "Default (recommended)", which is a concrete model rather than off.

Solution

Decline the model instead of failing per request. update_models_from_config now asks the model whether it can authenticate before selecting it, and leaves _inline_completion_model unset when it cannot, so handle_inline_completions returns before building any context. A second guard inside inline_completions covers callers that construct the model directly, such as extensions.

Ask the SDK, do not re-derive its precedence. The load-bearing decision is _client_can_authenticate. An earlier draft read the client's api_key and auth_token, which was wrong in three ways: on current SDKs Anthropic also authenticates from a credentials provider (ANTHROPIC_PROFILE, a config-dir profile, or the workload-identity variables) and from an X-Api-Key or Authorization header supplied through ANTHROPIC_CUSTOM_HEADERS, and each of those leaves both attributes unset while requests succeed. Judging by attributes would have switched auto-complete off for deployments where it works today. The check now calls the SDK's own header validation, the code that raises the error above, with the client's merged default headers.

Every uncertain step allows the request, because a wrong "no" (auto-complete off for a working deployment) is worse than a wrong "yes" (the error this guard exists to prevent). A validator that is absent, that will not accept two header arguments, or headers that are not a mapping all read as authenticated. The argument count is checked with inspect.signature(...).bind(...) rather than assumed: treating a signature TypeError as if it were the authentication one would disable a working setup on an SDK whose validator changed shape, which is a failure this guard demonstrably had before that check was added.

A credential that carries nothing reads as absent, a deliberate divergence from the SDK. ANTHROPIC_AUTH_TOKEN= (how .env and compose files usually spell "unset"), a whitespace-only value, and a blank credential passed through ANTHROPIC_CUSTOM_HEADERS all leave the SDK willing to send an empty auth header; the API then answers 401 for every completion request, which is the same flood in a different exception. The settings panel already normalized whitespace, so this closes the gap where the identical value in the environment behaved differently.

One warning per server run. update_models_from_config re-runs on every /capabilities GET, and the front end refetches that on startup, after a settings save, and on every Claude CLI status change, building a fresh model each time. A per-model flag coalesced nothing, so the warning lives in a module-scope helper, matching the existing github_copilot default-password warning.

A credential problem no longer fails an unrelated surface. The SDK reads profile files while resolving credentials, so a bad ANTHROPIC_CONFIG_DIR raises inside the model constructor. That runs from the capabilities handler, so it is now caught and logged rather than failing the whole response.

The Status card agrees with the server. readiness.py reported a missing key as harmless ("This is fine if the Claude CLI is signed in") and considered only ANTHROPIC_API_KEY. It now accepts ANTHROPIC_AUTH_TOKEN and says plainly that auto-complete is disabled, while keeping the subscription-login hedge for chat.

Testing

  • Reproduced first, offline. The model constructs cleanly with an api_key of None, "", or whitespace and raises the reported TypeError on inline_completions, on upstream/main.
  • Oracle verified against SDK ground truth. Each candidate check was compared with _build_request across eight credential shapes: absent, blank api_key, blank auth token, both blank, a real key, a real auth token, a custom-header key, and a workload-identity provider. The shipped check agrees with the SDK everywhere except the intentional blank-credential case above.
  • Python tests. New coverage for: no credential returning no suggestion rather than raising; settings, env, and auth-token credentials counting as configured; a credentials provider and both custom-header shapes counting as configured (the three false negatives review found); blank env credentials not counting; an absent validator allowing the request and a rejecting one refusing it; the warning being emitted once across both the selection and request paths; a configured model still issuing its request, with the mock's credential pinned so the assertion is real, plus a negative counterpart; the manager leaving the model unset with no credential, selecting it with either a settings or env credential, and surviving a constructor that raises. Two readiness tests cover the auth-token source and the reworded row.
  • Suites. pytest tests/ --ignore=tests/test_claude_client.py: 1787 passed. tsc --noEmit, eslint, prettier, stylelint clean. jest: 423 passed across 33 suites (no frontend sources changed).
  • Test isolation against an ambient credential. The negative tests construct real SDK clients, so an on-disk profile at ~/.config/anthropic/configs/default.json would authenticate them and make five of them fail on a developer machine that has one. Clearing the environment cannot reach that, and pointing ANTHROPIC_CONFIG_DIR at an empty directory makes construction raise instead, so the fixtures stub the SDK's credential discovery. Measured with a fake home containing such a profile: discovery resolves a credential without the stub and resolves none with it, and the guard tests pass under that home. The one test that needs real discovery, the workload-identity case, restores it for its own duration.
  • Live in JupyterLab 4.x, in an isolated HOME so the real config was untouched (NBIConfig hardcodes ~/.jupyter/nbi/config.json, so JUPYTER_CONFIG_DIR cannot redirect it). With Claude mode on and no credential: one warning naming the remedy, zero tracebacks, and typing past the debounce added no further log lines. With a placeholder key in the environment and the same config: no warning, and a real request to /v1/models returning 401, which confirms the credential path was taken rather than skipped. Six /capabilities refreshes produced one warning, where the pre-remediation build produced three. The readiness row rendered as "No Anthropic credential is visible to the server. Claude auto-complete is disabled."

Risks / follow-ups

  • Behavior change. A keyless Claude-mode user who previously saw tracebacks now gets no auto-complete suggestions at all, which is the honest state, plus one log line and a Status card row saying so. Chat is unaffected.
  • A gateway that injects auth. A custom base_url fronting a proxy that adds credentials server-side, with no key configured in NBI, is treated as unauthenticated. That configuration already failed on main with the same TypeError per keystroke, so nothing working is lost, but there is no override to force auto-complete on.
  • Private SDK method. The check calls the SDK's header validation, which is private API. The fallback is permissive, so an SDK change degrades to the previous behavior rather than disabling the feature.
  • The frontend still shows the Claude auto-complete icon while the model is gated off, because src/index.ts keys that off the configured setting rather than what the server resolved, and it still sends one request per debounce, which now returns immediately. Both predate this change (the same is true of the none setting today) and are worth a follow-up that surfaces the resolved state in the capabilities response.
  • Inline chat has the same root cause and still reports a generic failure message when no credential is present. It fails once per explicit user action rather than continuously, so it is left for a follow-up that can reuse this message.
  • Not in scope: tests/test_claude_models.py::TestGetContextWindow fails intermittently in full-suite runs and passes in isolation, on upstream/main as well as here. Nothing in this change touches litellm or that code path. The failures track machine load (that test's litellm import measured 1.66s idle and 4.87s under load, against the 30s per-test cap in pyproject.toml), and it failed once in a run where these new tests were explicitly deselected, so it is independent of this work. I could not reproduce the tipping deliberately, so I am not asserting a mechanism or attempting a fix.
  • Pre-existing test hygiene, worth a follow-up: every test using test_ai_service_manager_integration.py's _make_manager_for_update_test helper builds a real AIServiceManager, which constructs ClaudeCodeChatParticipant and spawns an actual claude CLI child process. Fourteen such tests predate this change and the four added here follow the same house pattern, so nothing new is introduced, but pointing NBI_CLAUDE_CLI_PATH at a nonexistent path in the test environment would make that file hermetic. That file's run-to-run timing also varies widely; I looked into it and could not establish a cause (removing the CLI from PATH and forcing the agent connect timeout to 0.1s both failed to reduce it), so I am reporting it as unexplained rather than guessing. The new tests themselves cost about half a second to a second per file, at or below the noise on this machine.

Closes #425

Claude Code mode signs in through the Claude CLI, which accepts a
subscription login, but inline completions call the Anthropic API
directly and need a credential of their own. With none available the SDK
raised TypeError while building each request, so every pause in typing
left a fresh traceback in the server log.

Model selection now asks whether the SDK resolved a credential before
choosing the Claude auto-complete model, and leaves the feature off when
it did not, which also skips the context building that precedes the
request. The check runs the SDK's own header validation rather than
inspecting an API key, because an attribute check would disable
auto-complete for deployments where it works today: a profile, the
workload-identity variables, and a credential passed through
ANTHROPIC_CUSTOM_HEADERS all leave api_key and auth_token unset while
requests authenticate. Every step that cannot be carried out
confidently allows the request, including a validator of another shape,
so an SDK change cannot silently switch the feature off.

A credential that carries nothing counts as absent. A blank
ANTHROPIC_AUTH_TOKEN, a whitespace value, or a blank custom header all
leave the SDK willing to send an empty auth header, and the API answers
401 apiece, which is the same flood in a different exception.

The warning is emitted once per server run, shared by the selection and
request paths, and re-arms when a credential reappears so a key removed
later is not silent. Selection re-runs on every capabilities request, so
a per-model flag would coalesce nothing. Model construction is wrapped
as well: the SDK reads profile files while resolving credentials, so a
bad ANTHROPIC_CONFIG_DIR raised there and failed the whole capabilities
response over one feature's misconfiguration.
…rmless

The Anthropic credentials row considered only ANTHROPIC_API_KEY, so a
server authenticating with ANTHROPIC_AUTH_TOKEN was reported as having
no credential. _credential_source now takes several environment
variables and the row accepts either.

The row also spoke only about chat turns, while a missing credential
definitely disables auto-complete. It now names auto-complete alongside
chat in the remedy, but deliberately does not assert that either is off:
this check sees one settings field and two environment variables, while
the SDK also authenticates from a profile, the workload-identity
variables, and custom headers, so claiming a feature is disabled would
be false on exactly those deployments and would send an operator looking
for a credential they already have.
Records the fix in the unreleased 5.4.0 section and adds a
troubleshooting entry for the state a keyless Claude-mode user lands in:
what counts as a credential, that a process environment variable has to
be exported before JupyterLab starts, that a blank value does not count,
that an administrator-pinned auto-complete model leaves supplying a
credential as the only remedy, and that a misconfigured profile logs a
different message than a missing credential does.
@pjdoland
pjdoland force-pushed the fix/425-inline-completion-missing-key branch from 8618ff5 to 11f3b09 Compare September 14, 2026 13:15
@pjdoland pjdoland added the bug Something isn't working label Sep 14, 2026
@pjdoland
pjdoland requested a review from mbektas September 14, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

It started to crash

1 participant