qoder

package
v0.1.863 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: MIT, MIT Imports: 23 Imported by: 0

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

View Source
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).

View Source
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.

View Source
const (
	// BodyAlphabet is the custom base64 alphabet.
	BodyAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!"
)
View Source
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.

View Source
const CNProviderKey = "qoder-cn"

CNProviderKey is how magpie names the Qoder CN subscription; it is also the id of Qoder CN's CLI agent.

View Source
const ProviderKey = "qoder"

ProviderKey is how magpie names this subscription.

View Source
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.

View Source
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

View Source
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.

View Source
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

func BodyDecode(s string) []byte

BodyDecode decodes custom base64 (no third swap).

func BodyEncode

func BodyEncode(data []byte) string

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 ChatURL

func ChatURL() string

ChatURL is the global site's chat endpoint.

func DecodeRequestBody

func DecodeRequestBody(wire string) []byte

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

func EncodeRequestBody(plaintext []byte) string

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

func IsRoutableModel(key string) bool

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

func ParseModels(body []byte, provider string) ([]catalog.Model, error)

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) JobToken

func (f *DeviceFlow) JobToken(ctx context.Context, deviceToken string) (*JobToken, error)

JobToken trades a device token for the job token the model calls use.

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

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.

func (JobToken) Expiry

func (r JobToken) Expiry() time.Duration

Expiry is the job token's lifetime as a duration; zero when Qoder said none.

type JobTokenHTTPError added in v0.1.528

type JobTokenHTTPError struct {
	StatusCode int
	Body       string
}

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 shows it: a price_factor of 0 (see free).
	Free bool `json:"-"`
	// Rate is the credits a request costs, as a multiple (its
	// price_factor), and RateWas the price struck through beside it while
	// a discount runs (see free); 0 when the listing says none.
	Rate    float64 `json:"-"`
	RateWas float64 `json:"-"`
}

ModelInfo is one entry of the model-list response's "chat" array. Only the fields magpie uses are kept.

func ModelConfigs

func ModelConfigs(body []byte) ([]ModelInfo, error)

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.

func SiteOf added in v0.1.528

func SiteOf(id string) *Site

SiteOf is the site a provider id or a credential's Site names; anything but Qoder CN, including none, is the global site.

func (*Site) ChatURL added in v0.1.528

func (s *Site) ChatURL() string

ChatURL is the full chat endpoint on the site's inference host.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL