Documentation
¶
Overview ¶
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.
Index ¶
- Constants
- Variables
- func BodyDecode(s string) []byte
- func BodyEncode(data []byte) string
- func BuildCosyHeaders(rawURL string, user *User, body string, timestamp int64) (map[string]string, error)
- func ChatURL() string
- func DecodeRequestBody(wire string) []byte
- func DeviceExpiry(t DeviceToken) time.Time
- func EncodeRequestBody(plaintext []byte) string
- func FetchModels(ctx context.Context, client *http.Client, baseURL string, user *User) ([]byte, error)
- func FetchUsage(ctx context.Context, client *http.Client, baseURL, deviceToken string) (json.RawMessage, error)
- func IsRoutableModel(key string) bool
- func MachineOS() string
- func NewID() string
- func NewMachineID() string
- func ParseModels(body []byte, provider string) ([]catalog.Model, error)
- type Credential
- type DeviceFlow
- func (f *DeviceFlow) Authorization() (authURL, verifier, nonce string, err error)
- func (f *DeviceFlow) Client() *http.Client
- func (f *DeviceFlow) JobToken(ctx context.Context, deviceToken string) (*JobToken, error)
- func (f *DeviceFlow) MachineID() string
- func (f *DeviceFlow) PollDeviceToken(ctx context.Context, nonce, verifier string, interval time.Duration) (*DeviceToken, error)
- func (f *DeviceFlow) Site() *Site
- type DeviceToken
- type DeviceTokenRefreshHTTPError
- type JobToken
- type JobTokenHTTPError
- type JobTokenRefreshHTTPError
- type ModelInfo
- type Site
- type UsageHTTPError
- type User
- type UserInfo
Constants ¶
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://" )
OAuth device-flow configuration (verified against live captures).
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" )
API endpoints.
const (
// BodyAlphabet is the custom base64 alphabet.
BodyAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!"
)
const CNClientID = "e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb"
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 CNProviderKey = "qoder-cn"
CNProviderKey is how magpie names the Qoder CN subscription; it is also the id of Qoder CN's CLI agent.
const ProviderKey = "qoder"
ProviderKey is how magpie names this subscription.
const RSAPublicKeyPEM = `` /* 271-byte string literal not displayed */
RSAPublicKeyPEM is Qoder's embedded 1024-bit RSA public key (from the client's cosy source), used to encrypt the per-request AES key that wraps the user blob in the COSY Authorization header.
const UserInfoPath = "/api/v1/userinfo"
UserInfoPath is the account endpoint, asked with the dt- device token (not the jt- job token). It answers who the signed-in account is.
Variables ¶
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}
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 Global = &Site{ID: ProviderKey, Name: "Qoder", Web: DeviceFlowHost, OpenAPI: OpenAPIHost, API: APIHost, ClientID: ClientID, RedirectURI: RedirectURI}
Global is qoder.com, what magpie's Qoder subscription has always used.
Functions ¶
func BodyDecode ¶
BodyDecode decodes custom base64 (no third swap).
func BodyEncode ¶
BodyEncode encodes one plaintext into custom base64 (no third swap).
func BuildCosyHeaders ¶
func BuildCosyHeaders(rawURL string, user *User, body string, timestamp int64) (map[string]string, error)
BuildCosyHeaders builds the full set of COSY request headers for a Qoder call to rawURL with body already in its wire (encoded) form.
func DecodeRequestBody ¶
DecodeRequestBody reverses the swap, then decodes.
func DeviceExpiry ¶ added in v0.1.528
func DeviceExpiry(t DeviceToken) time.Time
DeviceExpiry is when a device token used for chat is taken to lapse: what Qoder said, else a day, as a job token without a lifetime is.
func EncodeRequestBody ¶
EncodeRequestBody is the wire body for an agent_chat_generation request: custom base64, then the outer-third swap.
func FetchModels ¶
func FetchModels(ctx context.Context, client *http.Client, baseURL string, user *User) ([]byte, error)
FetchModels retrieves the raw model listing for a signed-in user. The request carries the same COSY envelope the client uses for every /algo call; an empty body is signed for a GET.
func FetchUsage ¶
func FetchUsage(ctx context.Context, client *http.Client, baseURL, deviceToken string) (json.RawMessage, error)
FetchUsage retrieves the account usage envelope using a device token; an empty baseURL is the global site's account host.
func IsRoutableModel ¶
IsRoutableModel reports whether a model key names one model an agent can pick. Aggregate entries ("auto", "default") route inside Qoder and aren't.
func MachineOS ¶
func MachineOS() string
MachineOS uses the architecture and platform names used by desktop clients.
func NewID ¶
func NewID() string
NewID is a UUID without dashes, the shape Qoder's request ids and tool-call ids use.
func NewMachineID ¶
func NewMachineID() string
NewMachineID generates the stable identity kept with one account.
func ParseModels ¶
ParseModels decodes the listing and returns the enabled, routable chat models as magpie's catalog entries. Placeholder ids ("auto", "default") are dropped: they route between models inside Qoder and aren't a single model an agent can pick. provider is the magpie provider id the entries are listed under (ProviderKey or CNProviderKey).
Types ¶
type Credential ¶
type Credential struct {
UID string `json:"uid"`
Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"`
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
DeviceToken string `json:"device_token"`
DeviceRefresh string `json:"device_refresh,omitempty"`
ExpiresAt int64 `json:"expires_at"`
MachineID string `json:"machine_id"`
Models json.RawMessage `json:"models,omitempty"`
// Site is the Qoder site the account is on: CNProviderKey for Qoder CN,
// empty for the global site (every credential saved before there were two).
Site string `json:"site,omitempty"`
// DeviceChat says Token is the device token itself, as Qoder CN's CLI
// uses it, because the site refused to trade it for a job token; it is
// then refreshed as a device token.
DeviceChat bool `json:"device_chat,omitempty"`
}
Credential is kept in the account's auth blob in magpie's logins.json.
func (*Credential) OnSite ¶ added in v0.1.528
func (c *Credential) OnSite() *Site
OnSite is the Qoder site the credential belongs to.
func (Credential) Refresh ¶
func (c Credential) Refresh(ctx context.Context, client *http.Client) (Credential, error)
Refresh returns a new credential value. The caller serializes the entire check, refresh and save operation, since refresh tokens rotate on use.
func (*Credential) Valid ¶
func (c *Credential) Valid() bool
type DeviceFlow ¶
type DeviceFlow struct {
// contains filtered or unexported fields
}
DeviceFlow runs Qoder's sign-in: an authorization page the user opens, then a poll for the device token, then its exchange for the job token that serves models. It is a helper, not a state machine: the caller owns the browser and the waiting, and calls these one at a time.
func NewDeviceFlow ¶
func NewDeviceFlow(client *http.Client, site *Site) *DeviceFlow
NewDeviceFlow makes a flow on site (nil is the global one); a nil client is a 20-second-timeout one.
func (*DeviceFlow) Authorization ¶
func (f *DeviceFlow) Authorization() (authURL, verifier, nonce string, err error)
Authorization returns the page to open and the (verifier, nonce) the poll needs. It is a PKCE device flow: a random verifier is kept, its S256 digest is what the page sees.
func (*DeviceFlow) Client ¶
func (f *DeviceFlow) Client() *http.Client
Client is the flow's HTTP client, reused for the model fetch after sign-in.
func (*DeviceFlow) MachineID ¶
func (f *DeviceFlow) MachineID() string
func (*DeviceFlow) PollDeviceToken ¶
func (f *DeviceFlow) PollDeviceToken(ctx context.Context, nonce, verifier string, interval time.Duration) (*DeviceToken, error)
PollDeviceToken asks for the device token until the user has authorized it, ctx ends the wait, or interval elapses between tries.
func (*DeviceFlow) Site ¶ added in v0.1.528
func (f *DeviceFlow) Site() *Site
Site is the Qoder site the flow signs in on.
type DeviceToken ¶
type DeviceToken struct {
Token string `json:"token"`
DeviceToken string `json:"device_token,omitempty"`
RefreshToken string `json:"refresh_token"`
UserID string `json:"user_id"`
UserName string `json:"user_name,omitempty"`
// ExpiresIn (seconds) or ExpiresAt (a date) is the token's lifetime, as
// Qoder's CLI reads it; either may be missing.
ExpiresIn int64 `json:"expires_in,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
}
DeviceToken is the dt- device token a finished device flow returns.
func RefreshDeviceToken ¶
func RefreshDeviceToken(ctx context.Context, client *http.Client, endpoint, refreshToken string) (*DeviceToken, error)
RefreshDeviceToken rotates the device token used by account endpoints; an empty endpoint is the global site's.
func (DeviceToken) Expiry ¶ added in v0.1.528
func (t DeviceToken) Expiry() time.Time
Expiry is when the device token lapses; zero when Qoder said nothing.
type DeviceTokenRefreshHTTPError ¶
type DeviceTokenRefreshHTTPError struct {
StatusCode int
}
DeviceTokenRefreshHTTPError preserves a rejected refresh status without response data.
func (*DeviceTokenRefreshHTTPError) Error ¶
func (e *DeviceTokenRefreshHTTPError) Error() string
type JobToken ¶
type JobToken struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
// ExpiresIn is Qoder's lifetime for the token, in milliseconds.
ExpiresIn int64 `json:"expires_in"`
}
JobToken is the jt- task token that authorizes the model calls.
func RefreshJobToken ¶
func RefreshJobToken(ctx context.Context, client *http.Client, site *Site, refreshToken string) (*JobToken, error)
RefreshJobToken trades a job token's refresh token for a new pair on site (nil is the global one). The old refresh token is spent, so the caller must keep the new one.
type JobTokenHTTPError ¶ added in v0.1.528
JobTokenHTTPError is a refused device-to-job token exchange.
func (*JobTokenHTTPError) Error ¶ added in v0.1.528
func (e *JobTokenHTTPError) Error() string
type JobTokenRefreshHTTPError ¶
type JobTokenRefreshHTTPError struct {
StatusCode int
}
JobTokenRefreshHTTPError preserves a rejected job-token refresh status.
func (*JobTokenRefreshHTTPError) Error ¶
func (e *JobTokenRefreshHTTPError) Error() string
type ModelInfo ¶
type ModelInfo struct {
Key string `json:"key"`
Source string `json:"source,omitempty"`
Enable bool `json:"enable"`
DisplayName string `json:"display_name,omitempty"`
IsVL bool `json:"is_vl,omitempty"`
IsReasoning bool `json:"is_reasoning,omitempty"`
MaxInputTokens int `json:"max_input_tokens,omitempty"`
Efforts []string `json:"reasoning_efforts,omitempty"`
Config json.RawMessage `json:"-"`
// From thinking_config: whether the model can think, whether it can't
// stop, and the effort it thinks at unless asked for another.
Thinks bool `json:"-"`
AlwaysThinks bool `json:"-"`
DefaultEffort string `json:"-"`
// Free is set on a model that costs the plan no credits, as Qoder's
// client reads it: is_free, or a price_factor of 0.
Free bool `json:"-"`
}
ModelInfo is one entry of the model-list response's "chat" array. Only the fields magpie uses are kept.
func ModelConfigs ¶
ModelConfigs preserves each enabled model's complete upstream configuration.
type Site ¶ added in v0.1.528
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
}
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 UsageHTTPError ¶
type UsageHTTPError struct {
StatusCode int
}
UsageHTTPError preserves the upstream status without exposing response data.
func (*UsageHTTPError) Error ¶
func (e *UsageHTTPError) Error() string
type User ¶
type User struct {
UID string // the Qoder user id (x-gw-user-id / Cosy-User value)
Name string // display name, may be empty
Email string // account email, may be empty
Token string // the jt- Bearer token (security_oauth_token)
MachineID string // generated once at sign-in, saved with the account
}
User is the signed-in Qoder account a COSY envelope is built for.
type UserInfo ¶
type UserInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"`
Avatar string `json:"avatar"`
Source string `json:"source"`
OrgName string `json:"organization_name"`
HighestTier bool `json:"is_highest_tier"`
}
UserInfo is what the account endpoint says about a signed-in account.
func FetchUserInfo ¶
func FetchUserInfo(ctx context.Context, client *http.Client, site *Site, deviceToken string) (*UserInfo, error)
FetchUserInfo asks site's account endpoint (nil is the global one) with a device token. The device token is short-lived but outlasts the sign-in that just made it, so this runs during the flow while it is fresh.