diff --git a/routstr/core/admin.py b/routstr/core/admin.py index 783474a8..390e834c 100644 --- a/routstr/core/admin.py +++ b/routstr/core/admin.py @@ -2516,7 +2516,7 @@ async def get_provider_model(provider_id: int, model_id: str) -> dict[str, objec status_code=404, detail="Model not found for this provider" ) return _row_to_model( - row, apply_provider_fee=True, provider_fee=provider.provider_fee + row, apply_provider_fee=False, provider_fee=provider.provider_fee ).dict() # type: ignore @@ -2729,7 +2729,10 @@ async def get_provider_models(provider_id: int) -> dict[str, object]: raise HTTPException(status_code=404, detail="Provider not found") db_models = await list_models( - session=session, upstream_id=provider_id, include_disabled=True + session=session, + upstream_id=provider_id, + include_disabled=True, + apply_fees=False, ) upstream_models = [] @@ -2737,10 +2740,7 @@ async def get_provider_models(provider_id: int) -> dict[str, object]: if upstream_instance: try: raw_models = await upstream_instance.fetch_models() - upstream_models = [ - upstream_instance._apply_provider_fee_to_model(m) - for m in raw_models - ] + upstream_models = raw_models except Exception as e: logger.error( f"Failed to fetch models from {provider.provider_type}: {e}" diff --git a/routstr/payment/models.py b/routstr/payment/models.py index 4afbf45e..7e374662 100644 --- a/routstr/payment/models.py +++ b/routstr/payment/models.py @@ -230,6 +230,7 @@ async def list_models( session: AsyncSession, upstream_id: int, include_disabled: bool = False, + apply_fees: bool = True, ) -> list[Model]: from sqlmodel import select @@ -247,7 +248,7 @@ async def list_models( return [ _row_to_model( r, - apply_provider_fee=True, + apply_provider_fee=apply_fees, provider_fee=providers_by_id[r.upstream_provider_id].provider_fee if r.upstream_provider_id in providers_by_id else 1.01, diff --git a/ui/components/AddProviderModelDialog.tsx b/ui/components/AddProviderModelDialog.tsx index 807ecb22..009eb942 100644 --- a/ui/components/AddProviderModelDialog.tsx +++ b/ui/components/AddProviderModelDialog.tsx @@ -808,45 +808,6 @@ export function AddProviderModelDialog({ )} /> - ( - - Max Prompt Cost - - - - - - )} - /> - ( - - Max Completion Cost - - - - - - )} - /> - ( - - Max Total Cost - - - - - - )} - /> diff --git a/ui/lib/api/services/admin.ts b/ui/lib/api/services/admin.ts index 2ea27e7e..9298aaf4 100644 --- a/ui/lib/api/services/admin.ts +++ b/ui/lib/api/services/admin.ts @@ -139,18 +139,26 @@ export class AdminService { ): Record { if (!pricing) return pricing; const result = { ...pricing }; - if (typeof result.prompt === 'number') { - result.prompt = result.prompt * 1000000; - } - if (typeof result.completion === 'number') { - result.completion = result.completion * 1000000; - } - if (typeof result.request === 'number') { - result.request = result.request * 1000000; - } - if (typeof result.image === 'number') { - result.image = result.image * 1000000; - } + + // Only prompt and completion are per-token and need scaling to per-1M + const convertField = (field: string) => { + const val = result[field]; + if (val !== undefined && val !== null) { + const num = typeof val === 'string' ? parseFloat(val) : (val as number); + if (!isNaN(num)) { + // Multiply by 1M and round to avoid floating point artifacts (e.g. 0.40399999999999997) + // 9 decimals is plenty for USD/1M tokens (0.000000001) + result[field] = parseFloat((num * 1000000).toFixed(9)); + } + } + }; + + convertField('prompt'); + convertField('completion'); + + // Other fields (request, image, etc.) are already flat fees (per item) + // so we do NOT scale them. + return result; } @@ -159,18 +167,23 @@ export class AdminService { ): Record { if (!pricing) return pricing; const result = { ...pricing }; - if (typeof result.prompt === 'number') { - result.prompt = result.prompt / 1000000; - } - if (typeof result.completion === 'number') { - result.completion = result.completion / 1000000; - } - if (typeof result.request === 'number') { - result.request = result.request / 1000000; - } - if (typeof result.image === 'number') { - result.image = result.image / 1000000; - } + + // Only prompt and completion are per-1M in UI and need scaling down to per-token + const convertField = (field: string) => { + const val = result[field]; + if (val !== undefined && val !== null) { + const num = typeof val === 'string' ? parseFloat(val) : (val as number); + if (!isNaN(num)) { + result[field] = num / 1000000; + } + } + }; + + convertField('prompt'); + convertField('completion'); + + // Other fields stay as flat fees + return result; } @@ -291,7 +304,19 @@ export class AdminService { const data = await apiClient.get( `/admin/api/upstream-providers/${providerId}/models` ); - return data; + + // Convert pricing for all models in the list so the UI receives "per 1M tokens" values + return { + ...data, + db_models: data.db_models.map((m) => ({ + ...m, + pricing: this.convertPricingToPerMillionTokens(m.pricing), + })), + remote_models: data.remote_models.map((m) => ({ + ...m, + pricing: this.convertPricingToPerMillionTokens(m.pricing), + })), + }; } static async createProviderModel( @@ -498,8 +523,8 @@ export class AdminService { : null; const pricing = { - prompt: (data.input_cost as number) / 1000000, - completion: (data.output_cost as number) / 1000000, + prompt: data.input_cost as number, + completion: data.output_cost as number, request: (data.min_cost_per_request as number) || 0, image: 0, web_search: 0, @@ -553,8 +578,8 @@ export class AdminService { const existingModel = await this.getModel(modelId, providerId); const pricing = { - prompt: (data.input_cost as number) / 1000000, - completion: (data.output_cost as number) / 1000000, + prompt: data.input_cost as number, + completion: data.output_cost as number, request: (data.min_cost_per_request as number) || 0, image: 0, web_search: 0,