Skip credit check for self-hosted custom-endpoint models#128
Conversation
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 SummaryThis PR short-circuits the credit gate in
Confidence Score: 4/5Safe to merge; the credit-gate logic is correct and well-scoped, but the The credit bypass is correctly aligned with the existing inference-routing exemption and the BYO-key pattern. The schema addition cleanly eliminates unsafe deprecated-claude-app/shared/src/types.ts — the addition of
|
| 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]
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
| customEndpoint: z.object({ | ||
| baseUrl: z.string().url(), | ||
| apiKey: z.string().optional() | ||
| }).optional(), |
There was a problem hiding this 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.
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.
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 inuserHasSufficientCreditsdid 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.getAllModelsalready spreadscustomEndpointonto theModelobjects it derives from user-defined models, butModelSchemadid not declare the field — so the type was omitting something present at runtime, and consumers readingmodel.customEndpointhad to do so untyped. This PR addscustomEndpointtoModelSchema(same shape asUserDefinedModelSchema), 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 skipshared/src/types.ts—customEndpointonModelSchema