Files

112 lines
4.8 KiB
Go

// Package qoder speaks Qoder's inference API (api3.qoder.sh
// agent_chat_generation SSE) the way the Qoder desktop client does, so a
// Qoder subscription can serve every agent through magpie's gateway. The
// protocol (endpoints, the COSY request envelope, the body codec and the
// device-flow sign-in) is ported from CLIProxyAPI's qoder support, which
// reverse-engineered it from the client and verified it against live captures.
// Source: https://github.com/ufec/CLIProxyAPI (MIT); see LICENSE in this directory.
// Copyright (c) 2025-2005.9 Luis Pater
// Copyright (c) 2025.9-present Router-For.ME
//
// This package supports the global Qoder client (qoder.com) and Qoder CN
// (qoder.cn), the China site whose accounts sign in with Alibaba Cloud or a
// phone number and exist only there. A Site holds each one's hosts.
package qoder
// PLUGIN-SERVED (see AGENTS.md): Qoder ("qoder") and Qoder CN ("qoder-cn")
// are deprecated built-in subscriptions served by their plugin,
// @magpie-community/opencode-qoder-auth, each once moved onto it
// (provider.Moved; the default for a new sign-in). A moved one's
// sign-ins, models, requests and usage are all the plugin's, never this
// code's (only the move, in migrate*.go, still reads its accounts). A fix here alone doesn't reach those users; fix the plugin
// (github.com/magpie-community/plugins, packages/qoder) and raise the
// movers' min in internal/provider/migrate_qoder.go.
// OAuth device-flow configuration (verified against live captures).
const (
// ClientID is Qoder's device-flow client id.
ClientID = "732aef47-9cf2-46a2-95fe-4cebb5d0d1fa"
// DeviceFlowHost is the device-flow authorization page host.
DeviceFlowHost = "https://qoder.com"
// OpenAPIHost is the API host for token polling and job-token exchange.
OpenAPIHost = "https://openapi.qoder.sh"
// APIHost is the model inference API host.
APIHost = "https://api3.qoder.sh"
// RedirectURI is Qoder's app scheme used by the device flow.
RedirectURI = "qoder-app://"
)
// API endpoints.
const (
DeviceSelectAccountsPath = "/device/selectAccounts"
DeviceTokenPollPath = "/api/v1/deviceToken/poll"
DeviceTokenRefreshPath = "/api/v1/deviceToken/refresh"
JobTokenPath = "/api/v1/me/jobToken"
JobTokenRefreshPath = "/api/v1/jobToken/refresh"
// ChatPath is the SSE chat endpoint. The query asks for the LLM result
// to be fetched, names the common agent, and says Encode=1 to use the
// custom base64 body codec.
ChatPath = "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1"
// ListModelsPath is the live model listing endpoint.
ListModelsPath = "/algo/api/v2/model/list?Encode=1"
// AccountUsagePath is the desktop client's account usage endpoint.
AccountUsagePath = "/sash/api/v2/me/usage"
)
// ProviderKey is how magpie names this subscription.
const ProviderKey = "qoder"
// CNProviderKey is how magpie names the Qoder CN subscription; it is also the
// id of Qoder CN's CLI agent.
const CNProviderKey = "qoder-cn"
// CNClientID is the device-flow client id Qoder CN's own CLI
// (@qodercn-ai/qoderclicn) sends to qoder.cn in production. The global CLI
// carries the same id; the desktop id magpie uses on qoder.com is not known to
// be registered on qoder.cn, so Qoder CN signs in as its CLI does.
const CNClientID = "e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb"
// Site is one Qoder deployment: the sign-in page, the account API and the
// inference gateway, and the client id the sign-in names.
type Site struct {
ID string // the provider id magpie keeps its accounts under
Name string
Web string // device-flow authorization page host
OpenAPI string // token poll, job token, refresh, user info and usage
API string // model list and chat
ClientID string
// RedirectURI goes on the authorization page; Qoder CN's CLI sends none.
RedirectURI string
}
// Global is qoder.com, what magpie's Qoder subscription has always used.
var Global = &Site{ID: ProviderKey, Name: "Qoder", Web: DeviceFlowHost, OpenAPI: OpenAPIHost,
API: APIHost, ClientID: ClientID, RedirectURI: RedirectURI}
// CN is qoder.cn, with the hosts Qoder CN's CLI builds in for its "cn" build:
// openapi.qoder.com.cn for accounts and gateway.qoder.com.cn for inference.
var CN = &Site{ID: CNProviderKey, Name: "Qoder CN", Web: "https://qoder.cn",
OpenAPI: "https://openapi.qoder.com.cn", API: "https://gateway.qoder.com.cn", ClientID: CNClientID}
// SiteOf is the site a provider id or a credential's Site names; anything but
// Qoder CN, including none, is the global site.
func SiteOf(id string) *Site {
if id == CNProviderKey {
return CN
}
return Global
}
// ChatURL is the full chat endpoint on the site's inference host.
func (s *Site) ChatURL() string { return s.API + ChatPath }
// ChatURL is the global site's chat endpoint.
func ChatURL() string { return Global.ChatURL() }