mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-28 00:47:07 +00:00
allow max-tokenless models on openrouter
This commit is contained in:
@@ -587,6 +587,64 @@ describe('ChatCompletionDriver.complete credit gate and max_tokens cap', () => {
|
||||
expect(completeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// A provider that can't report a model's output ceiling used to make the
|
||||
// cap arithmetic go negative — `null - approxTokens` is negative, not NaN
|
||||
// — so a funded account was told it had insufficient funds.
|
||||
for (const [label, ceiling] of [
|
||||
['null', null],
|
||||
['zero', 0],
|
||||
['undefined', undefined],
|
||||
] as const) {
|
||||
it(`serves models whose output ceiling is ${label}`, async () => {
|
||||
vi.spyOn(
|
||||
FakeChatProvider.prototype,
|
||||
'models',
|
||||
).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'nocap',
|
||||
aliases: [],
|
||||
costs_currency: 'usd-cents',
|
||||
costs: { input_tokens: 1000, output_tokens: 2000 },
|
||||
max_tokens: ceiling,
|
||||
},
|
||||
] as never);
|
||||
const d = await makeDriver();
|
||||
|
||||
vi.spyOn(
|
||||
server.services.metering,
|
||||
'hasEnoughCredits',
|
||||
).mockResolvedValue(true);
|
||||
vi.spyOn(
|
||||
server.services.metering,
|
||||
'getRemainingUsage',
|
||||
).mockResolvedValue(100_000);
|
||||
|
||||
const completeSpy = vi
|
||||
.spyOn(FakeChatProvider.prototype, 'complete')
|
||||
.mockResolvedValueOnce({
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'ok' }],
|
||||
},
|
||||
usage: { input_tokens: 1, output_tokens: 1 },
|
||||
finish_reason: 'stop',
|
||||
} as never);
|
||||
|
||||
await withTestActor(() =>
|
||||
d.complete({
|
||||
model: 'nocap',
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
}),
|
||||
);
|
||||
|
||||
// Credits still bound the request; only the unknown model
|
||||
// ceiling is ignored.
|
||||
const passed = completeSpy.mock.calls[0]![0] as ICompleteArguments;
|
||||
expect(passed.max_tokens!).toBeGreaterThan(0);
|
||||
expect(passed.max_tokens!).toBeLessThanOrEqual(50);
|
||||
});
|
||||
}
|
||||
|
||||
it('rejects subscriber-only models for the default free subscription', async () => {
|
||||
vi.spyOn(FakeChatProvider.prototype, 'models').mockResolvedValueOnce([
|
||||
{
|
||||
|
||||
@@ -407,11 +407,19 @@ export class ChatCompletionDriver extends PuterDriver {
|
||||
remainingCredits - approximateInputCost;
|
||||
const maxAllowedOutputTokens =
|
||||
maxAllowedOutputUcents / outputTokenCost;
|
||||
// A provider may not know a model's output ceiling. Drop the
|
||||
// term rather than let a missing value drive the cap: `null`
|
||||
// coerces to 0, so the subtraction goes negative instead of
|
||||
// NaN and the user is told they're out of credits.
|
||||
const modelOutputCeiling =
|
||||
Number.isFinite(model.max_tokens) && model.max_tokens > 0
|
||||
? model.max_tokens - approximateTokenCount
|
||||
: Number.POSITIVE_INFINITY;
|
||||
const cap = Math.floor(
|
||||
Math.min(
|
||||
args.max_tokens ?? Number.POSITIVE_INFINITY,
|
||||
maxAllowedOutputTokens,
|
||||
model.max_tokens - approximateTokenCount,
|
||||
modelOutputCeiling,
|
||||
),
|
||||
);
|
||||
// `cap` is the credit-bounded ceiling on output tokens. When
|
||||
|
||||
@@ -105,6 +105,15 @@ const SAMPLE_API_MODELS = [
|
||||
pricing: { prompt: 0.000002, completion: 0.00001 },
|
||||
top_provider: { max_completion_tokens: 8192 },
|
||||
},
|
||||
{
|
||||
// OpenRouter reports no separate output cap for some models.
|
||||
id: 'meta/muse-spark-1.2',
|
||||
name: 'Muse Spark 1.2',
|
||||
created: 1786032000,
|
||||
context_length: 1048576,
|
||||
pricing: { prompt: 0.00000125, completion: 0.00000425 },
|
||||
top_provider: { max_completion_tokens: null },
|
||||
},
|
||||
{
|
||||
// 'openrouter/auto' is filtered out — disallowed.
|
||||
id: 'openrouter/auto',
|
||||
@@ -228,6 +237,25 @@ describe('OpenRouterProvider model catalog', () => {
|
||||
expect(axiosRequestMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('falls back to the context window when no output cap is reported', async () => {
|
||||
const { provider } = makeProvider();
|
||||
const models = await provider.models();
|
||||
|
||||
// A null max_completion_tokens previously landed on the model as-is,
|
||||
// which made the driver's cap arithmetic go negative and reject the
|
||||
// request as insufficient funds.
|
||||
const noCap = models.find(
|
||||
(m) => m.id === 'openrouter:meta/muse-spark-1.2',
|
||||
)!;
|
||||
expect(noCap.max_tokens).toBe(1048576);
|
||||
|
||||
// A model that reports its own cap keeps it.
|
||||
const capped = models.find(
|
||||
(m) => m.id === 'openrouter:openai/gpt-5-nano',
|
||||
)!;
|
||||
expect(capped.max_tokens).toBe(16000);
|
||||
});
|
||||
|
||||
it('maps OpenRouter created timestamps to release_date metadata', async () => {
|
||||
const { provider } = makeProvider();
|
||||
const models = await provider.models();
|
||||
|
||||
@@ -72,9 +72,11 @@ export class OpenRouterProvider implements IChatProvider {
|
||||
}
|
||||
/**
|
||||
* Returns a list of available model names including their aliases
|
||||
*
|
||||
* Retrieves all available model IDs and their aliases, flattening them into
|
||||
* a single array of strings that can be used for model selection
|
||||
*
|
||||
* @returns {Promise<string[]>} Array of model identifiers and their aliases
|
||||
* @description Retrieves all available model IDs and their aliases,
|
||||
* flattening them into a single array of strings that can be used for model selection
|
||||
*/
|
||||
async list() {
|
||||
const models = await this.models();
|
||||
@@ -85,10 +87,7 @@ export class OpenRouterProvider implements IChatProvider {
|
||||
return model_names;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Chat completion method.
|
||||
* See AIChatService for more details.
|
||||
*/
|
||||
/** AI Chat completion method. See AIChatService for more details. */
|
||||
async complete({
|
||||
messages,
|
||||
stream,
|
||||
@@ -279,7 +278,11 @@ export class OpenRouterProvider implements IChatProvider {
|
||||
model.id.split('/').slice(1).join('/'),
|
||||
],
|
||||
context: model.context_length,
|
||||
max_tokens: model.top_provider.max_completion_tokens,
|
||||
// OpenRouter leaves max_completion_tokens null when a model
|
||||
// declares no output cap separate from its context window.
|
||||
max_tokens:
|
||||
model.top_provider.max_completion_tokens ??
|
||||
model.context_length,
|
||||
costs_currency: 'usd-cents',
|
||||
input_cost_key: 'prompt',
|
||||
output_cost_key: 'completion',
|
||||
|
||||
Reference in New Issue
Block a user