AI classification: support both Moonshot-direct and OpenRouter back ends #113
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
FenLiu's AI classification (text + vision) is built around a single OpenAI-compatible endpoint, currently Moonshot direct (
https://api.moonshot.ai/v1, modelkimi-k2.6). Two assumptions inservices/ai_classification.pyare Moonshot-specific:"thinking": {"type": "disabled"}. That is a Moonshot parameter; OpenRouter does not advertisethinkingas a supported parameter formoonshotai/kimi-k2.6(its unified parameter isreasoning).message.reasoning_content, Moonshot's field name. OpenRouter returns reasoning undermessage.reasoningandmessage.reasoning_detailsand has noreasoning_contentfield at all.Pointing FenLiu at OpenRouter is therefore not a drop-in configuration change. Requests may be forwarded with parameters an upstream provider does not support, and diagnostics that inspect reasoning content silently report it as absent even when the model reasoned.
OpenRouter's non-streaming envelope also differs structurally from Moonshot direct: it adds
native_finish_reasonandlogprobsto each choice,provider/service_tier/system_fingerprintat top level,refusalon the message, andcost/cost_details/is_byokonusage.A second wrinkle applies to OpenRouter's shared capacity (i.e. not BYOK).
moonshotai/kimi-k2.6is an open-weight model served by roughly twenty upstream providers, and their supported-parameter sets differ — at least one (Chutes) does not advertiseresponse_format, which FenLiu relies on for{"type": "json_object"}. BYOK routes to the Moonshot AI endpoint specifically; shared capacity may land on any of the others. The response envelope itself is OpenRouter-normalized in both cases.Desired outcome
FenLiu should work reliably against either backend — Moonshot direct (
kimi-k2.6) or OpenRouter (moonshotai/kimi-k2.6) — selected purely by configuration, with classification verdicts unchanged.Impact
Today an operator who wants to switch to OpenRouter (or fall back to it) cannot do so safely with configuration alone.
Confirmation: the JSON really does differ
Verified live against both endpoints with the same trivial prompt (HTTP 200 on each). Side-by-side of the response envelopes:
api.moonshot.ai,kimi-k2.6)openrouter.ai,moonshotai/kimi-k2.6)"thinking": {"type": "disabled"}"reasoning": {...}—thinkingis not in the model'ssupported_parametersfinish_reason,index,message+ native_finish_reason,+ logprobscontent,reasoning_content,rolecontent,reasoning,reasoning_details,refusal,role— noreasoning_contentprovider,service_tier,system_fingerprintcost,cost_details,is_byok,prompt_tokens_detailsA reasoning-enabled OpenRouter call returned
message.reasoning(a string) andmessage.reasoning_details(an array), withusage.completion_tokens_details.reasoning_tokens: 152. The Moonshot-direct call had noreasoningfield at all — it usesreasoning_content. So the_malformed_reply_diagnosticshelper from #106, which readsmessage.reasoning_content, will always reportreasoning_content=absentfor OpenRouter responses.BYOK vs shared capacity (the "not BYOK" part)
The envelope is OpenRouter-normalized either way; BYOK does not change the shape. What "not BYOK" changes is which upstream provider serves the open-weight model. The model endpoints listing on OpenRouter shows ~20 providers for
moonshotai/kimi-k2.6(Baidu, DeepInfra, Fireworks, Moonshot AI, Chutes, …). Their capability sets differ: every one advertisesreasoning, but none advertisesthinking, and at least Chutes does not advertiseresponse_format. A request that works BYOK (routed to the Moonshot AI endpoint) can therefore land somewhere with weaker parameter support on shared capacity.Two more observations:
"thinking"param without error (the routed provider understands it), so the request is not rejected today — but it is undocumented for this model and not the portable choice."response_format": {"type": "json_object"}works on the Moonshot AI endpoint and most shared providers, but is not universally advertised.Plan of attack
reasoning_content) and an OpenRouter-shaped envelope (reasoning/reasoning_details/native_finish_reason/cost/is_byok) and assert the diagnostic reports reasoning as present for each. Add a test asserting which reasoning-disable request parameter is sent per backend._malformed_reply_diagnosticstolerate both field names (preferreasoning, fall back toreasoning_content); keepmessage.contenthandling as-is since both use it.thinkingfor Moonshot direct, send the unifiedreasoningfor OpenRouter — derived from the configured base URL (possibly an explicit small provider enum rather than string-sniffing).response_format, considerstructured_outputs/prompt-enforced JSON or a retry; decide scope during review.docs/user-guide/ai-classification.mdand the configuration table with both base-URL/model combinations.pylock.tomlonly if dependencies change (none expected).Findings from live verification are above; no dependencies or schema changes needed.