Skip to content

Skip credit check for self-hosted custom-endpoint models#128

Open
simpolism wants to merge 1 commit into
anima-research:mainfrom
simpolism:jake.skip-credit-check-custom-endpoint
Open

Skip credit check for self-hosted custom-endpoint models#128
simpolism wants to merge 1 commit into
anima-research:mainfrom
simpolism:jake.skip-credit-check-custom-endpoint

Conversation

@simpolism
Copy link
Copy Markdown
Contributor

Models configured with their own OpenAI-compatible endpoint (customEndpoint.baseUrl) have no platform cost basis: pricing resolves to $0 and the inference path already bypasses the API-key manager for them (InferenceService.isCustomModelWithEndpoint). But the credit gate in userHasSufficientCredits did not account for this, so a user with no grant balance is blocked with "Insufficient credits" from a model that costs the platform nothing to serve.

Fix

Skip the credit check when the model carries a customEndpoint, mirroring the existing "user brought their own API key" exemption directly below it in the same function.

Type correctness

ModelLoader.getAllModels already spreads customEndpoint onto the Model objects it derives from user-defined models, but ModelSchema did not declare the field — so the type was omitting something present at runtime, and consumers reading model.customEndpoint had to do so untyped. This PR adds customEndpoint to ModelSchema (same shape as UserDefinedModelSchema), making the access in the credit gate type-safe.

Tradeoff worth flagging

This bakes in the assumption that a self-hosted custom endpoint is never credit-metered. That is correct when the platform has no cost relationship with the endpoint (the operator runs it themselves). A multi-tenant host that wanted to meter even BYO-endpoint usage would want this behind a config flag instead. Happy to gate it if that is a concern for the project's deployment model.

Files

  • backend/src/websocket/handler.ts — credit-gate skip
  • shared/src/types.tscustomEndpoint on ModelSchema

Models configured with their own openai-compatible endpoint (customEndpoint.baseUrl)
have no platform cost basis: pricing resolves to $0 and the inference path already
bypasses the API-key manager for them (InferenceService.isCustomModelWithEndpoint).
The credit gate in userHasSufficientCredits did not account for this, so a user with
no grant balance was blocked with "Insufficient credits" from a model that costs the
platform nothing to serve.

Skip the credit check when the model carries a customEndpoint, mirroring the existing
"user brought their own API key" exemption.

Also add the customEndpoint field to ModelSchema. ModelLoader.getAllModels already
spreads it onto Model objects derived from user-defined models, so the type was
omitting a field that exists at runtime; declaring it lets consumers read it safely.
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 3, 2026

Greptile Summary

This PR short-circuits the credit gate in userHasSufficientCredits for models that carry a customEndpoint, preventing users of self-hosted OpenAI-compatible endpoints from being incorrectly blocked by a credit check on a zero-platform-cost model. It also formalizes customEndpoint on ModelSchema to make the runtime field type-safe for all consumers.

  • Credit bypass (handler.ts): the new guard fires before the currency-balance query and mirrors the existing BYO-key exemption; inference routing already had an equivalent bypass (isCustomModelWithEndpoint), so this brings the credit gate into alignment.
  • Schema change (types.ts): customEndpoint is added to ModelSchema with the same shape as UserDefinedModelSchema, eliminating the (model as any).customEndpoint casts in inference.ts. The optional apiKey sub-field is now an official part of the shared type, which means it will be serialized into client-facing API responses from GET /api/models (res.json(models)).

Confidence Score: 4/5

Safe to merge; the credit-gate logic is correct and well-scoped, but the apiKey field newly formalized in ModelSchema is returned verbatim in client-facing model listing responses.

The credit bypass is correctly aligned with the existing inference-routing exemption and the BYO-key pattern. The schema addition cleanly eliminates unsafe (model as any) casts. The one concern worth a second look is that customEndpoint.apiKey is now an official part of the shared ModelSchema, and the authenticated GET /api/models route serializes the full model object — including that key — back to clients. The exposure is limited to authenticated users seeing their own credentials, but returning secrets in a general listing endpoint is worth hardening before this pattern propagates further.

deprecated-claude-app/shared/src/types.ts — the addition of apiKey to ModelSchema and how it flows into GET /api/models responses deserves a second look.

Security Review

  • Credential exposure in model listing (shared/src/types.ts lines 185-188): customEndpoint.apiKey is now part of ModelSchema and is returned verbatim by GET /api/models (res.json(models)). Authenticated users receive their own custom-endpoint API keys in every model listing response, widening the exposure surface beyond what is needed for the credit-gate fix alone.

Important Files Changed

Filename Overview
deprecated-claude-app/backend/src/websocket/handler.ts Adds an early-return in userHasSufficientCredits that skips the credit check when a model carries a customEndpoint; logic mirrors the existing BYO-key exemption and is correctly placed before the currency balance query.
deprecated-claude-app/shared/src/types.ts Adds customEndpoint (including the optional apiKey field) to ModelSchema; shape matches UserDefinedModelSchema correctly, but including apiKey in the shared schema means it is serialized and returned to clients by the authenticated model listing endpoint.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[WebSocket message with modelId] --> B[userHasSufficientCredits]
    B --> C{modelId provided?}
    C -- No --> G[Check grant balance]
    C -- Yes --> D[getModelById userId]
    D --> E{model found?}
    E -- No --> G
    E -- Yes --> F{model.customEndpoint?}
    F -- Yes --> PASS1[return true ✅\nskip credit check\nnew in this PR]
    F -- No --> H{user has provider API key?}
    H -- Yes --> PASS2[return true ✅\nexisting BYO-key path]
    H -- No --> G[Query grant currencies & balance]
    G --> I{positive balance\nor overspend?}
    I -- Yes --> PASS3[return true ✅]
    I -- No --> FAIL[return false ❌\nInsufficient credits]
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
deprecated-claude-app/shared/src/types.ts:185-188
**API key included in model listing responses**

`GET /api/models` calls `res.json(models)` verbatim (see `backend/src/routes/models.ts` line 23), which now officially includes `customEndpoint.apiKey` for every user-defined model returned. While the route is authenticated and scoped per user, returning credentials in a listing endpoint unnecessarily broadens the exposure surface: any XSS payload, intercepted response, or accidental logging picks up the key alongside regular model metadata. Consider either omitting `apiKey` from `ModelSchema` (keeping it only on `UserDefinedModelSchema`) and stripping it in `getAllModels`, or adding a `ModelPublicSchema = ModelSchema.omit({ ... })` passed to client-facing routes.

Reviews (1): Last reviewed commit: "fix: skip credit check for self-hosted c..." | Re-trigger Greptile

Comment on lines +185 to +188
customEndpoint: z.object({
baseUrl: z.string().url(),
apiKey: z.string().optional()
}).optional(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 security API key included in model listing responses

GET /api/models calls res.json(models) verbatim (see backend/src/routes/models.ts line 23), which now officially includes customEndpoint.apiKey for every user-defined model returned. While the route is authenticated and scoped per user, returning credentials in a listing endpoint unnecessarily broadens the exposure surface: any XSS payload, intercepted response, or accidental logging picks up the key alongside regular model metadata. Consider either omitting apiKey from ModelSchema (keeping it only on UserDefinedModelSchema) and stripping it in getAllModels, or adding a ModelPublicSchema = ModelSchema.omit({ ... }) passed to client-facing routes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: deprecated-claude-app/shared/src/types.ts
Line: 185-188

Comment:
**API key included in model listing responses**

`GET /api/models` calls `res.json(models)` verbatim (see `backend/src/routes/models.ts` line 23), which now officially includes `customEndpoint.apiKey` for every user-defined model returned. While the route is authenticated and scoped per user, returning credentials in a listing endpoint unnecessarily broadens the exposure surface: any XSS payload, intercepted response, or accidental logging picks up the key alongside regular model metadata. Consider either omitting `apiKey` from `ModelSchema` (keeping it only on `UserDefinedModelSchema`) and stripping it in `getAllModels`, or adding a `ModelPublicSchema = ModelSchema.omit({ ... })` passed to client-facing routes.

How can I resolve this? If you propose a fix, please make it concise.

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