Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions apps/web/src/app/api/openrouter/[...path]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
logFreeModelRequest,
} from '@/lib/free-model-rate-limiter';
import { gemma_4_26b_a4b_it_free_model } from '@/lib/ai-gateway/providers/google';
import { tencent_hy3_free_model } from '@/lib/ai-gateway/providers/tencent';
import { stepfun_37_flash_free_model } from '@/lib/ai-gateway/providers/stepfun';
import { getEffectiveModelDecision } from '@/lib/organizations/effective-model-access.server';
import { getPercentageRoutedPartnerProvider } from '@/lib/ai-gateway/providers/partner/routing';
import { FRIENDLI_GLM_PUBLIC_ID } from '@/lib/ai-gateway/providers/partner/constants';
Expand Down Expand Up @@ -608,7 +608,9 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () =>
} as unknown as Awaited<ReturnType<typeof getUserFromAuth>>);

const { POST } = await import('./route');
const response = await POST(makeRequest(makeBody(tencent_hy3_free_model.public_id)) as never);
const response = await POST(
makeRequest(makeBody(stepfun_37_flash_free_model.public_id)) as never
);

expect(response.status).toBe(200);
expect(mockedCheckFreeModelRateLimit).not.toHaveBeenCalled();
Expand Down
24 changes: 12 additions & 12 deletions apps/web/src/lib/ai-gateway/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,13 @@ describe('isFreeModel', () => {
expect(findKiloExclusiveModel('qwen/qwen3.7-plus')).toBeNull();
});

test('registers Tencent Hy3 as an Auto Free model', () => {
expect(findKiloExclusiveModel('tencent/hy3:free')).toBe(tencent_hy3_free_model);
expect(tencent_hy3_free_model.internal_id).toBe('tencent/hy3');
expect(tencent_hy3_free_model.inference_provider_restriction).toEqual(['tencent']);
expect(autoFreeModels.map(({ model }) => model)).toContain(tencent_hy3_free_model.public_id);
test('disables Tencent Hy3 free and excludes it from Auto Free and preferred models', () => {
expect(tencent_hy3_free_model.status).toBe('disabled');
expect(findKiloExclusiveModel('tencent/hy3:free')).toBeNull();
expect(autoFreeModels.map(({ model }) => model)).not.toContain(
tencent_hy3_free_model.public_id
);
expect(preferredModels).not.toContain(tencent_hy3_free_model.public_id);
});

test('registers LongCat 2.0 as an Auto Free model', async () => {
Expand Down Expand Up @@ -231,18 +233,16 @@ describe('isFreeModel', () => {
Object.fromEntries(autoFreeModels.map(({ model, reasoning }) => [model, reasoning]))
).toEqual({
'stepfun/step-3.7-flash:free': { enabled: true, effort: 'high' },
'tencent/hy3:free': { enabled: true, effort: 'high' },
'poolside/laguna-s-2.1:free': { enabled: true, effort: 'high' },
'meituan/longcat-2.0-free': { enabled: true, effort: 'high' },
});
});

test('weights Auto Free models at 70% StepFun and 10% each for Hy3, Laguna, and LongCat', () => {
test('weights Auto Free models at 7:1:1 for StepFun, Laguna, and LongCat', () => {
expect(
Object.fromEntries(autoFreeModels.map(({ model, weight }) => [model, weight]))
).toEqual({
'stepfun/step-3.7-flash:free': 7,
'tencent/hy3:free': 1,
'poolside/laguna-s-2.1:free': 1,
'meituan/longcat-2.0-free': 1,
});
Expand Down Expand Up @@ -409,12 +409,12 @@ describe('getKiloExclusiveInferenceProviderRestriction', () => {
expect(
getKiloExclusiveInferenceProviderRestriction(gpt_5_6_sol_discounted_model.public_id)
).toEqual(new Set(['openai']));
expect(getKiloExclusiveInferenceProviderRestriction(tencent_hy3_free_model.public_id)).toEqual(
new Set(['tencent'])
);
});

test('does not treat unrestricted exclusives or unknown ids as restricted', () => {
test('does not treat disabled or unrestricted exclusives or unknown ids as restricted', () => {
expect(
getKiloExclusiveInferenceProviderRestriction(tencent_hy3_free_model.public_id)
).toBeUndefined();
expect(
getKiloExclusiveInferenceProviderRestriction(gemma_4_26b_a4b_it_free_model.public_id)
).toBeUndefined();
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/ai-gateway/providers/tencent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const tencent_hy3_free_model: KiloExclusiveModel = {
'Hy3 is a 295B-parameter Mixture-of-Experts model from Tencent, activating 21B parameters per token. It supports configurable reasoning effort, agentic workflows, reliable tool calling, and long-context tasks across coding, document processing, financial analysis, and frontend development.',
context_length: 262_144,
max_completion_tokens: 128_000,
status: 'public',
status: 'disabled',
flags: ['reasoning'],
gateway: 'openrouter',
internal_id: 'tencent/hy3',
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/lib/ai-gateway/unavailable-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ describe('unavailable models', () => {
expect(isUnavailableModel('sakana/fugu-ultra')).toBe(false);
expect(isUnavailableModel('google/gemma-4-31b-it:free')).toBe(true);
expect(isUnavailableModel('openai/gpt-oss-20b')).toBe(false);
expect(isUnavailableModel('tencent/hy3:free')).toBe(true);
expect(isUnavailableModel('tencent/hy3')).toBe(false);
});

test('matches normalized families for provider metadata', () => {
expect(familyHasUnavailableFreeModel('openai/gpt-oss-20b:free')).toBe(true);
expect(familyHasUnavailableFreeModel('openai/gpt-oss-20b')).toBe(true);
expect(familyHasUnavailableFreeModel('cohere/north-mini-code')).toBe(false);
expect(familyHasUnavailableFreeModel('tencent/hy3:free')).toBe(true);
expect(familyHasUnavailableFreeModel('tencent/hy3')).toBe(true);
});
});
1 change: 1 addition & 0 deletions apps/web/src/lib/ai-gateway/unavailable-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const unavailableModelIds: ReadonlySet<string> = new Set([
'nvidia/nemotron-nano-12b-v2-vl:free',
'nvidia/nemotron-nano-9b-v2:free',
'openai/gpt-oss-20b:free',
'tencent/hy3:free',
'z-ai/glm-5.2:free',
]);

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/tests/openrouter-models-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ describe('OpenRouter Models Config', () => {
CLAUDE_OPUS_CURRENT_MODEL_ID,
GPT_CURRENT_MODEL_ID,
'z-ai/glm-5.3',
tencent_hy3_free_model.public_id,
];

expectedModels.forEach(model => {
Expand All @@ -29,6 +28,7 @@ describe('OpenRouter Models Config', () => {
'stealth/qwen3.6-plus',
QWEN37_PLUS_MODEL_ID,
'deepseek/deepseek-v4-pro',
tencent_hy3_free_model.public_id,
];

supersededModels.forEach(model => {
Expand Down