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
38 changes: 19 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,24 +82,24 @@ process.stdout.write("\n");

## Reference

| Export | Description |
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `createXaiResponsesAdapter` | `AdapterFactory` for the CLI chat proxy. Per-source `quirks` are ignored. |
| `xaiResponsesQuirks` | The `ResponsesQuirks` the adapter is built from. |
| `xaiOAuthConfig` | `OAuthClientConfig` for `@corbits/oauth-core`: client id, endpoints, loopback redirect, scopes. |
| `exchangeXaiCode(code, verifier, now, fetch?)` | Redeems an authorization code for `XaiTokens`. |
| `refreshXaiTokens(refresh, now, fetch?)` | Refreshes `XaiTokens`, keeping the old refresh token when the response omits one. |
| `xaiTokensFromResponse(response, now, previousRefresh?)` | Maps a token endpoint response to `XaiTokens`, carrying `id_token` as `idToken`. |
| `xaiUserIdFromAccessToken(access)` | Reads the JWT `sub` claim. Returns `undefined` on malformed input; does not verify signatures. |
| `XaiTokens` | Type. |
| `XAI_PROVIDER` | The `"xai"` provider id. |
| `XAI_OAUTH_PROXY_BASE_URL` | `https://cli-chat-proxy.grok.com/v1`, the base URL for OAuth credentials. |
| `XAI_API_KEY_BASE_URL` | `https://api.x.ai/v1`, the base URL for API key credentials. |
| `XAI_DEFAULT_MODELS` | The model ids the CLI chat proxy serves. |
| `XAI_USER_ID_OPTION` | `providerOptions` key sent as the `x-grok-user-id` header. |
| `XAI_SESSION_ID_OPTION` | `providerOptions` key sent as `prompt_cache_key`. |
| `XAI_REASONING_EFFORT_OPTION` | `providerOptions` key sent as `reasoning.effort`. |
| `XAI_REFRESH_SKEW_MS` | Refresh this long before expiry. |
| Export | Description |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `createXaiResponsesAdapter` | `AdapterFactory` for the CLI chat proxy. Per-source `quirks` accept only `{ userId? }`; any other key throws. |
| `xaiResponsesQuirks` | The `ResponsesQuirks` the adapter is built from. |
| `xaiOAuthConfig` | `OAuthClientConfig` for `@corbits/oauth-core`: client id, endpoints, loopback redirect, scopes. |
| `exchangeXaiCode(code, verifier, now, fetch?)` | Redeems an authorization code for `XaiTokens`. |
| `refreshXaiTokens(refresh, now, fetch?)` | Refreshes `XaiTokens`, keeping the old refresh token when the response omits one. |
| `xaiTokensFromResponse(response, now, previousRefresh?)` | Maps a token endpoint response to `XaiTokens`, carrying `id_token` as `idToken`. |
| `xaiUserIdFromAccessToken(access)` | Reads the JWT `sub` claim. Returns `undefined` on malformed input; does not verify signatures. |
| `XaiTokens` | Type. |
| `XAI_PROVIDER` | The `"xai"` provider id. |
| `XAI_OAUTH_PROXY_BASE_URL` | `https://cli-chat-proxy.grok.com/v1`, the base URL for OAuth credentials. |
| `XAI_API_KEY_BASE_URL` | `https://api.x.ai/v1`, the base URL for API key credentials. |
| `XAI_DEFAULT_MODELS` | The model ids the CLI chat proxy serves. |
| `XAI_USER_ID_OPTION` | `providerOptions` key sent as the `x-grok-user-id` header. |
| `XAI_SESSION_ID_OPTION` | `providerOptions` key sent as `prompt_cache_key`. |
| `XAI_REASONING_EFFORT_OPTION` | `providerOptions` key sent as `reasoning.effort`. |
| `XAI_REFRESH_SKEW_MS` | Refresh this long before expiry. |

The OAuth and client-identity constants behind `xaiOAuthConfig` and the adapter (`XAI_CLIENT_ID`, `XAI_AUTHORIZE_URL`, `XAI_TOKEN_URL`, `XAI_REDIRECT_URI`, `XAI_SCOPES`, `XAI_TOKEN_TIMEOUT_MS`, `XAI_RESPONSES_PATH`, `XAI_CLIENT_IDENTIFIER`, `XAI_CLIENT_VERSION`, `XAI_USER_AGENT`, `XAI_CLIENT_MODE`, `XAI_TOKEN_AUTH`, `XAI_AUTHENTICATE_RESPONSE`) are exported too.

Expand Down Expand Up @@ -141,7 +141,7 @@ const manifest: AdapterManifest = [
export const deps = createDependencies(await loadAdapterRegistry(manifest));
```

Each xAI source points at `XAI_OAUTH_PROXY_BASE_URL`, uses the OAuth access token as its credential, and picks a model from `XAI_DEFAULT_MODELS`. Pass `xaiUserIdFromAccessToken(token)` as `providerOptions[XAI_USER_ID_OPTION]`.
Each xAI source points at `XAI_OAUTH_PROXY_BASE_URL`, uses the OAuth access token as its credential, and picks a model from `XAI_DEFAULT_MODELS`. Pass `xaiUserIdFromAccessToken(token)` as `providerOptions[XAI_USER_ID_OPTION]`, or, for an Interchange catalog source, which carries no `providerOptions`, as the source's `quirks: { userId }`. The option overrides the quirk. The quirk must match the credential's account, so re-set it when the credential is signed in to another account.

## License

Expand Down
20 changes: 20 additions & 0 deletions src/responses-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,23 @@ describe("createXaiResponsesAdapter", () => {
expect(parsed).not.toHaveProperty("temperature");
});
});

describe("createXaiResponsesAdapter — source quirks", () => {
test("sends the userId quirk as x-grok-user-id, and the option overrides it", () => {
const adapter = createXaiResponsesAdapter(source, { userId: "user-quirk" });
expect(
adapter.buildRequest(turns, "grok-4.5", {}).headers["x-grok-user-id"],
).toBe("user-quirk");
expect(
adapter.buildRequest(turns, "grok-4.5", {
providerOptions: { xaiUserId: "user-option" },
}).headers["x-grok-user-id"],
).toBe("user-option");
});

test("rejects a userId that would split the header", () => {
expect(() => createXaiResponsesAdapter(source, { userId: "a\nb" })).toThrow(
/invalid quirks/,
);
});
});
32 changes: 29 additions & 3 deletions src/responses-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type ResponsesQuirks,
} from "@corbits/openai-responses";
import type { AdapterFactory } from "@intx/inference";
import { type } from "arktype";
import {
XAI_AUTHENTICATE_RESPONSE,
XAI_CLIENT_IDENTIFIER,
Expand Down Expand Up @@ -50,10 +51,35 @@ export const xaiResponsesQuirks: ResponsesQuirks = {
temperature: false,
};

// A catalog source carries no providerOptions, so a deployment states the
// user id here. Visible ASCII only, so a value that is not a valid header
// fails here rather than on every request.
const XaiSourceQuirks = type({
"userId?": /^[\x21-\x7e]+$/,
"+": "reject",
});

const createBaseAdapter = responsesAdapterFactory(xaiResponsesQuirks);

/**
* `AdapterFactory` for xAI's Responses-speaking CLI chat proxy, with xAI's
* headers, system-prompt placement, and reasoning shape baked in via
* `@corbits/openai-responses`.
* `@corbits/openai-responses`. The source's `quirks` may name the
* `userId` sent as `x-grok-user-id`; a per-call option overrides it.
*/
export const createXaiResponsesAdapter: AdapterFactory =
responsesAdapterFactory(xaiResponsesQuirks);
export const createXaiResponsesAdapter: AdapterFactory = (source, quirks) => {
const parsed = XaiSourceQuirks(quirks ?? {});
if (parsed instanceof type.errors)
throw new Error(`@corbits/xai-provider: invalid quirks: ${parsed.summary}`);
if (parsed.userId === undefined) return createBaseAdapter(source);
return responsesAdapterFactory({
...xaiResponsesQuirks,
headers: {
...xaiResponsesQuirks.headers,
static: {
...xaiResponsesQuirks.headers?.static,
"x-grok-user-id": parsed.userId,
},
},
})(source);
};
Loading