Skip to content

feat(ui): migrate the tags hooks to the typed fetchClient - #29886

Closed
ryan-crabbe-berri wants to merge 7 commits into
litellm_internal_stagingfrom
litellm_ui_migrate_tags
Closed

feat(ui): migrate the tags hooks to the typed fetchClient#29886
ryan-crabbe-berri wants to merge 7 commits into
litellm_internal_stagingfrom
litellm_ui_migrate_tags

Conversation

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor

Relevant issues

Stacks on #29884 (the typed fetchClient). Until that merges, this PR's diff also shows the fetchClient foundation; once it lands the diff reduces to the tags changes.

Linear ticket

N/A

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all unit tests
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Type

Refactoring

Changes

First domain migrated onto the typed fetch pattern. useTags previously called the hand-written tagListCall networking helper; it now calls fetchClient.GET("/tag/list") and reads .data, keeping its TagListResponse contract and its existing query key. The auth token is injected by the client middleware, so the hook no longer threads accessToken into the call; it still gates the query on accessToken, userId, and userRole exactly as before

The test mocks fetchClient instead of networking and keeps the meaningful coverage: it asserts the hook fetches /tag/list, unwraps the body on success, surfaces errors, and does not fetch when any auth value is missing. Each test rebuilds its own QueryClient so there is no cross-test cache bleed

To stop the folder from regressing, a no-restricted-imports override bans @/components/networking imports under src/app/(dashboard)/hooks/tags. The ban patterns (this one and the existing tremor ban) are pulled into shared constants so the override does not duplicate the tremor rule. This is the per-folder enforcement ratchet that will flip on for each domain as it is migrated, the same mechanism that already bans raw fetch outside src/lib/http

Screenshots / Proof of Fix

The Tags page reads its data through useTags, so it exercises the new client end to end. With a proxy running on localhost:4000:

  1. Start the proxy: python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --reload --use_v2_migration_resolver 2>&1 | tee litellm.log
  2. Open the dashboard and go to the Tags page (Tag Management)
  3. Confirm the tag list renders as before
  4. In the browser network tab, confirm the request to /tag/list carries the bearer auth header and returns 200 through the new client

…al fetch pattern

Introduces fetchClient (openapi-fetch) bound to schema.d.ts so dashboard data fetching infers paths, query params, and request bodies from the proxy's OpenAPI spec. It is used inside ordinary TanStack Query hooks (fetchClient.GET("/path")), keeping the existing useQuery plus query-key-factory style rather than a wrapper API.

A small runtime registry feeds the client the mutable base URL and auth header name (registered by networking) and the session token (published by AuthContext), so call sites carry no token plumbing. Auth-header injection and ApiError mapping live in openapi-fetch middleware, reusing deriveErrorMessage and ApiError from the existing http client; because non-2xx maps to a thrown ApiError, query functions just read .data.

Untyped responses (most GETs lack a response_model, so they arrive as unknown) are cast through a single documented UntypedApiResponse alias to keep call sites uniform. Converts useModelHub to fetchClient.GET("/model_group/info") as the first call site; legacy networking helpers are untouched.
Converts useTags from the legacy tagListCall networking helper to fetchClient.GET("/tag/list"), keeping its TagListResponse contract and query key. The auth token is injected by the client middleware, so the hook no longer threads accessToken into the call. The test mocks fetchClient instead of networking and keeps the success, error, and auth-gating coverage.

Adds a no-restricted-imports override for src/app/(dashboard)/hooks/tags so the folder cannot reach back for networking helpers; the ban patterns are factored into shared constants to avoid duplicating the existing tremor ban. First domain migrated onto the pattern from the fetchClient PR (#29884), on which this stacks.

Branch CI note: schema.d.ts is generated; CI regenerates and diffs it via gen:api.
@codecov

codecov Bot commented Jun 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces the typed openapi-fetch client infrastructure (lib/http/api.ts, lib/http/runtime.ts) and migrates the first two hooks — useTags and useModelHub — off the hand-written networking.tsx helpers. Auth token injection and URL rebasing are handled by middleware in api.ts, with AuthContext and networking.tsx feeding the runtime module, and per-folder ESLint bans enforcing the migration ratchet.

  • New fetchClient singleton (api.ts + runtime.ts): openapi-fetch client with middleware that rebases URLs onto the runtime base, injects the auth header, and maps non-2xx responses to ApiError; backed by thorough unit tests.
  • useTags migration: drops tagListCall in favour of fetchClient.GET("/tag/list"); the null-body fallback ?? {} is correct, but the TagListResponse return type was removed unnecessarily (see inline comment).
  • useModelHub migration: same pattern; the ESLint ban for hooks/tags/** implements the per-domain enforcement ratchet described in the PR.

Confidence Score: 4/5

Safe to merge after restoring the TagListResponse return type in useTags; all runtime behaviour is unchanged and the new client is well-tested.

The only real defect is in useTags.ts: the public return type was changed from the concrete TagListResponse to any (via UntypedApiResponse), even though TagListResponse is still importable from @/components/tag_management/types — a path not covered by the networking ban. Every existing caller that relied on TypeScript to catch misuse of tag data now gets untyped any instead. The rest of the change (runtime module, middleware, auth wiring, ESLint ratchet) is clean and correct.

ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTags.ts — the return type regression is the one thing to fix before merging.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTags.ts Migrates useTags to fetchClient, but drops the TagListResponse return type in favour of UntypedApiResponse (any), breaking type safety for callers unnecessarily.
ui/litellm-dashboard/src/lib/http/api.ts New typed openapi-fetch singleton with URL-rebasing and auth-header middleware; clean implementation with well-structured error handling.
ui/litellm-dashboard/src/lib/http/runtime.ts Module-level mutable state for base URL getter, auth header name getter, and auth token; sensible defaults and clear JSDoc.
ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts useModelHub migrated to fetchClient; no pre-existing typed return to preserve here, and data ?? {} fallback is correct.
ui/litellm-dashboard/eslint.config.mjs Extracts ban constants, adds per-folder networking import ban for hooks/tags — correct pattern for incremental migration enforcement.
ui/litellm-dashboard/src/contexts/AuthContext.tsx Adds setAuthToken calls alongside the existing setAccessToken, keeping the runtime module's token in sync with the auth context.
ui/litellm-dashboard/src/components/networking.tsx Registers the proxy base URL and auth header name getters into runtime.ts at module-load time; minimal and correct change.
ui/litellm-dashboard/src/lib/http/api.test.ts Thorough unit tests covering auth injection, URL rebasing, non-2xx error mapping, and successful body parsing.
ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTags.test.ts Tests updated to mock fetchClient instead of networking; covers success, error, no-body fallback, and all three auth-guard conditions.

Reviews (3): Last reviewed commit: "fix(ui): resolve the default request bas..." | Re-trigger Greptile

Comment thread ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTags.ts Outdated
Comment thread ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts Outdated
Comment on lines +46 to +47
export const fetchClient = createFetchClient<paths>({ baseUrl: globalThis.location?.origin ?? "" });
fetchClient.use(middleware);

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.

P2 Implicit initialization-order dependency on networking.tsx

fetchClient is a module-level singleton. Its middleware reads getRequestBaseUrl() and getAuthHeaderName(), which only return non-default values after networking.tsx has been imported and its two register* calls have run. If any consumer of fetchClient is bundled into a chunk that loads before networking.tsx (e.g. a lazy-loaded route that doesn't transitively import networking), every request in that chunk will silently use "" as the base URL and "Authorization" as the header name — both wrong if the proxy lives on a different origin or uses a custom header. Documenting the expected load order in the module JSDoc (or adding a runtime assertion) would make this coupling explicit.

Addresses review feedback on the migrated hooks. useTags now returns UntypedApiResponse (matching useModelHub and the untyped-endpoint convention) since /tag/list has no response_model yet; swap in the real type once the backend declares it. Both query functions guard a missing 2xx body with ?? {} so an empty response resolves to an empty object instead of undefined cast to a value type, with a regression test for each.

Leaves the api.ts load-order note (base URL / header getters registered by networking) for a follow-up.
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

Addresses the load-order coupling review comment. fetchClient's middleware reads the base URL from runtime.ts, which defaulted to "" until networking.tsx ran its register call, so a request firing before networking imported would target same-origin even on a split-origin deployment. The default now resolves from the NEXT_PUBLIC_BASE_URL build constant via the shared resolveApiBase, so it is correct without depending on networking loading first; networking still overrides it with the fuller runtime value including the UI-config server root path. The auth header name has no build-time source and stays "Authorization" until the JWT supplies a custom one.

Adds runtime.test.ts covering the env-resolved default and the header-name default.
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

The UntypedApiResponse (any) return broke build-ui: create_key_button does Object.values(tagsData).map((tag) => tag.name), and Object.values(any) infers unknown[] under this tsconfig, so tag.name failed to type-check. /tag/list has a good hand-written response type, so useTags keeps TagListResponse (the empty-body guard stays). useModelHub stays UntypedApiResponse since /model_group/info has no modeled response.
…yped

Tags was a poor first target: /tag/list has no response_model, so the openapi-fetch version only kept the hand-written TagListResponse behind a cast and broke a consumer when reduced to any. Restore useTags, its test, and the eslint config to the original networking-based version. Tags can be migrated later once the endpoint has a response_model so it is fully typed end to end.
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

Superseding this with #29884, which is the clean foundation off staging plus the first fully-typed caller migration (useCustomers). #29886 had picked up an unrelated, merge-induced schema drift and was migrating an untyped endpoint (tags); #29884 avoids both. Tags will be migrated later once /tag/list has a response_model.

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