@bubblebrain-ai/bubble 0.0.47 → 0.0.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/provider-grok.d.ts
CHANGED
|
@@ -9,9 +9,21 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { OAuthCredentials } from "./oauth/types.js";
|
|
11
11
|
import { type ChatGptFetch } from "./network/chatgpt-transport.js";
|
|
12
|
+
import type { ThinkingLevel } from "./types.js";
|
|
12
13
|
export declare const GROK_SUBSCRIPTION_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
|
|
13
14
|
export declare const GROK_CLIENT_VERSION = "0.2.93";
|
|
14
15
|
export declare function isGrokSubscriptionBaseUrl(baseURL: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Infer reasoning levels for a Grok model id the /models endpoint returned
|
|
18
|
+
* without metadata. Flagship reasoning models (grok-N.M) expose
|
|
19
|
+
* low/medium/high with thinking always on — matching the curated grok-4.5
|
|
20
|
+
* entry — while composer/fast/mini/flash variants have no effort control and
|
|
21
|
+
* map to the plain "off" ladder.
|
|
22
|
+
*/
|
|
23
|
+
export declare function inferGrokReasoningLevels(modelId: string): {
|
|
24
|
+
levels: ThinkingLevel[];
|
|
25
|
+
defaultLevel?: ThinkingLevel;
|
|
26
|
+
};
|
|
15
27
|
export declare function buildGrokSubscriptionHeaders(): Record<string, string>;
|
|
16
28
|
export interface GrokAuthAdapter {
|
|
17
29
|
getCredentials: () => OAuthCredentials | undefined | Promise<OAuthCredentials | undefined>;
|
|
@@ -24,3 +36,17 @@ export interface GrokAuthAdapter {
|
|
|
24
36
|
* proactively refreshes) the stored OAuth credentials per request instead.
|
|
25
37
|
*/
|
|
26
38
|
export declare function createGrokSubscriptionFetch(auth: GrokAuthAdapter, baseFetch?: ChatGptFetch): ChatGptFetch;
|
|
39
|
+
/**
|
|
40
|
+
* Fetch the model list for a Grok subscription account. The CLI chat proxy
|
|
41
|
+
* gates on the grok-cli identity headers plus a fresh OAuth bearer, so this
|
|
42
|
+
* rides the same refreshing fetch used for chat requests instead of a raw
|
|
43
|
+
* global fetch. Throws on a non-2xx so callers fall back to the curated
|
|
44
|
+
* catalog when the proxy exposes no /models endpoint.
|
|
45
|
+
*/
|
|
46
|
+
export declare function fetchGrokSubscriptionModels(baseURL: string, auth: GrokAuthAdapter, options?: {
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
fetch?: ChatGptFetch;
|
|
49
|
+
}): Promise<Array<{
|
|
50
|
+
id: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
}>>;
|
package/dist/provider-grok.js
CHANGED
|
@@ -17,6 +17,20 @@ export function isGrokSubscriptionBaseUrl(baseURL) {
|
|
|
17
17
|
const normalized = baseURL.trim().replace(/\/+$/, "");
|
|
18
18
|
return normalized === GROK_SUBSCRIPTION_BASE_URL || normalized.startsWith(`${GROK_SUBSCRIPTION_BASE_URL}/`);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Infer reasoning levels for a Grok model id the /models endpoint returned
|
|
22
|
+
* without metadata. Flagship reasoning models (grok-N.M) expose
|
|
23
|
+
* low/medium/high with thinking always on — matching the curated grok-4.5
|
|
24
|
+
* entry — while composer/fast/mini/flash variants have no effort control and
|
|
25
|
+
* map to the plain "off" ladder.
|
|
26
|
+
*/
|
|
27
|
+
export function inferGrokReasoningLevels(modelId) {
|
|
28
|
+
const id = modelId.toLowerCase();
|
|
29
|
+
if (/(composer|fast|mini|flash)/.test(id)) {
|
|
30
|
+
return { levels: ["off"] };
|
|
31
|
+
}
|
|
32
|
+
return { levels: ["low", "medium", "high"], defaultLevel: "high" };
|
|
33
|
+
}
|
|
20
34
|
export function buildGrokSubscriptionHeaders() {
|
|
21
35
|
return {
|
|
22
36
|
"User-Agent": "grok-cli",
|
|
@@ -57,3 +71,28 @@ export function createGrokSubscriptionFetch(auth, baseFetch = getChatGptFetch())
|
|
|
57
71
|
return baseFetch(input, { ...(init ?? {}), headers });
|
|
58
72
|
};
|
|
59
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Fetch the model list for a Grok subscription account. The CLI chat proxy
|
|
76
|
+
* gates on the grok-cli identity headers plus a fresh OAuth bearer, so this
|
|
77
|
+
* rides the same refreshing fetch used for chat requests instead of a raw
|
|
78
|
+
* global fetch. Throws on a non-2xx so callers fall back to the curated
|
|
79
|
+
* catalog when the proxy exposes no /models endpoint.
|
|
80
|
+
*/
|
|
81
|
+
export async function fetchGrokSubscriptionModels(baseURL, auth, options = {}) {
|
|
82
|
+
const fetchImpl = createGrokSubscriptionFetch(auth, options.fetch);
|
|
83
|
+
const base = baseURL.trim().replace(/\/+$/, "");
|
|
84
|
+
const response = await fetchImpl(`${base}/models`, {
|
|
85
|
+
headers: buildGrokSubscriptionHeaders(),
|
|
86
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 5_000),
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok)
|
|
89
|
+
throw new Error(`HTTP ${response.status}`);
|
|
90
|
+
const payload = (await response.json());
|
|
91
|
+
const entries = payload.data ?? payload.models ?? [];
|
|
92
|
+
return entries
|
|
93
|
+
.filter((entry) => typeof entry?.id === "string" && entry.id.trim().length > 0)
|
|
94
|
+
.map((entry) => ({
|
|
95
|
+
id: entry.id,
|
|
96
|
+
name: typeof entry.name === "string" ? entry.name : undefined,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
@@ -9,7 +9,7 @@ import { type ProviderProtocol } from "./model-catalog.js";
|
|
|
9
9
|
import { ModelConfig } from "./model-config.js";
|
|
10
10
|
import { AuthStorage } from "./oauth/index.js";
|
|
11
11
|
import { type OpenAICodexAuthAdapter } from "./provider-openai-codex.js";
|
|
12
|
-
import type
|
|
12
|
+
import { type GrokAuthAdapter } from "./provider-grok.js";
|
|
13
13
|
import type { ThinkingLevel } from "./types.js";
|
|
14
14
|
export interface ProviderProfile {
|
|
15
15
|
id: string;
|
|
@@ -10,6 +10,7 @@ import { ModelConfig } from "./model-config.js";
|
|
|
10
10
|
import { AuthStorage } from "./oauth/index.js";
|
|
11
11
|
import { fetchGeminiModels } from "./provider-ai-sdk.js";
|
|
12
12
|
import { extractChatGptAccountId, fetchOpenAICodexModelCatalog } from "./provider-openai-codex.js";
|
|
13
|
+
import { fetchGrokSubscriptionModels, inferGrokReasoningLevels } from "./provider-grok.js";
|
|
13
14
|
import { refreshOpenAICodex } from "./oauth/openai-codex.js";
|
|
14
15
|
import { refreshGrok } from "./oauth/grok.js";
|
|
15
16
|
/** True for providers speaking the OpenAI chat-completions protocol (the default). */
|
|
@@ -521,6 +522,45 @@ export class ProviderRegistry {
|
|
|
521
522
|
return this.fallbackDiscovery(provider, error);
|
|
522
523
|
}
|
|
523
524
|
}
|
|
525
|
+
// Grok subscription: the CLI chat proxy gates on the grok-cli identity
|
|
526
|
+
// headers and a fresh OAuth bearer, neither of which the generic path below
|
|
527
|
+
// sends. Discover through the refreshing subscription fetch, then merge the
|
|
528
|
+
// curated catalog with any remote-only ids so newly-released models surface
|
|
529
|
+
// without a code change. Authoritative because the merged list is what the
|
|
530
|
+
// routing catalog and model picker should treat as the account's catalog.
|
|
531
|
+
if (provider.id === "grok" && provider.authType === "oauth" && provider.apiKey) {
|
|
532
|
+
try {
|
|
533
|
+
await this.prepareProvider("grok");
|
|
534
|
+
const currentProvider = this.getConfigured().find((item) => item.id === "grok") ?? provider;
|
|
535
|
+
const grokAuth = this.createGrokAuthAdapter("grok");
|
|
536
|
+
if (!grokAuth)
|
|
537
|
+
throw new Error("Grok OAuth credentials are unavailable.");
|
|
538
|
+
const remote = await fetchGrokSubscriptionModels(currentProvider.baseURL, grokAuth);
|
|
539
|
+
const local = this.localModelsForProvider(currentProvider);
|
|
540
|
+
const known = new Set(local.map((model) => model.id));
|
|
541
|
+
const extras = remote
|
|
542
|
+
.filter((entry) => !known.has(entry.id) && isLikelyChatModelId(entry.id))
|
|
543
|
+
.map((entry) => {
|
|
544
|
+
const inferred = inferGrokReasoningLevels(entry.id);
|
|
545
|
+
return {
|
|
546
|
+
id: entry.id,
|
|
547
|
+
name: entry.name || entry.id,
|
|
548
|
+
providerId: currentProvider.id,
|
|
549
|
+
reasoningLevels: inferred.levels,
|
|
550
|
+
...(inferred.defaultLevel ? { defaultReasoningLevel: inferred.defaultLevel } : {}),
|
|
551
|
+
};
|
|
552
|
+
})
|
|
553
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
554
|
+
return {
|
|
555
|
+
models: [...local, ...extras],
|
|
556
|
+
source: "remote",
|
|
557
|
+
authoritative: true,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
return this.fallbackDiscovery(provider, error);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
524
564
|
// Generic OpenAI-compatible discovery: most vendors expose GET /models on
|
|
525
565
|
// the same base URL. Probed against every configured provider, their lists
|
|
526
566
|
// turned out to be neither complete nor clean — zhipuai omits glm-5.2,
|
|
@@ -642,7 +682,9 @@ export class ProviderRegistry {
|
|
|
642
682
|
? "openai-codex"
|
|
643
683
|
: provider.id === "google" && provider.protocol === "ai-sdk"
|
|
644
684
|
? "google"
|
|
645
|
-
:
|
|
685
|
+
: provider.id === "grok"
|
|
686
|
+
? "grok"
|
|
687
|
+
: undefined;
|
|
646
688
|
if (!dynamicProviderId)
|
|
647
689
|
return;
|
|
648
690
|
if (!result.authoritative) {
|
|
@@ -653,8 +695,11 @@ export class ProviderRegistry {
|
|
|
653
695
|
id: model.id,
|
|
654
696
|
name: model.name,
|
|
655
697
|
providerId: dynamicProviderId,
|
|
656
|
-
//
|
|
657
|
-
|
|
698
|
+
// A remote model with no trusted effort metadata still has to be
|
|
699
|
+
// selectable: default it to the plain "off" level (no reasoning control)
|
|
700
|
+
// rather than an empty ladder, which the model picker would render
|
|
701
|
+
// "unavailable" and skip during arrow-key navigation.
|
|
702
|
+
reasoningLevels: model.reasoningLevels ?? ["off"],
|
|
658
703
|
defaultReasoningLevel: model.defaultReasoningLevel,
|
|
659
704
|
contextWindow: model.contextWindow,
|
|
660
705
|
useResponsesLite: model.useResponsesLite,
|
|
@@ -8,7 +8,7 @@ import { useTheme } from "./theme.js";
|
|
|
8
8
|
import { filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
|
|
9
9
|
import { bareImageFilenameFromPaste, extractImagePathTokens, ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
|
|
10
10
|
import { appendHistoryEntry, loadHistoryEntriesSync, pushHistoryEntry, stepHistory, } from "./input-history.js";
|
|
11
|
-
import { cloneComposerBuffer, createComposerBuffer, deleteComposerBackward, deleteComposerForward, expandComposerBuffer,
|
|
11
|
+
import { cloneComposerBuffer, createComposerBuffer, deleteComposerBackward, deleteComposerForward, expandComposerBuffer, insertComposerPaste, insertComposerText, moveComposerCursor, replaceComposerRange, } from "./composer-buffer.js";
|
|
12
12
|
import { isKeyReleaseEvent } from "./key-events.js";
|
|
13
13
|
import { PasteOperationTracker } from "./paste-operation-tracker.js";
|
|
14
14
|
import { stripTerminalMouseSequences } from "./terminal-mouse.js";
|
|
@@ -26,6 +26,18 @@ export function createComposerDraftSnapshot(input) {
|
|
|
26
26
|
imageLabelStartOverride: input.imageLabelStartOverride,
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Rebuild a composer buffer from a persisted history entry. Text long enough to
|
|
31
|
+
* have been collapsed at paste time is collapsed again into a `[Pasted text #N …]`
|
|
32
|
+
* marker, but its full content is preserved in the buffer's paste spans so the
|
|
33
|
+
* composer stays compact, no content is lost, and the next submit re-expands it.
|
|
34
|
+
*/
|
|
35
|
+
function bufferFromHistoryText(text) {
|
|
36
|
+
if (shouldCollapsePastedContent(text)) {
|
|
37
|
+
return insertComposerPaste(createComposerBuffer(), text);
|
|
38
|
+
}
|
|
39
|
+
return createComposerBuffer(text);
|
|
40
|
+
}
|
|
29
41
|
/**
|
|
30
42
|
* Browse persisted history without turning marker-shaped text into live paste
|
|
31
43
|
* tokens, while preserving the exact unsent draft in memory for the final
|
|
@@ -68,7 +80,7 @@ export function stepComposerHistory(state, history, direction) {
|
|
|
68
80
|
};
|
|
69
81
|
}
|
|
70
82
|
return {
|
|
71
|
-
buffer:
|
|
83
|
+
buffer: bufferFromHistoryText(result.text),
|
|
72
84
|
attachments: (result.images ?? []).map((attachment) => ({ ...attachment })),
|
|
73
85
|
imageLabelStartOverride: result.imageDisplayStart ?? null,
|
|
74
86
|
historyIndex: result.index,
|
|
@@ -716,10 +728,10 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
716
728
|
return;
|
|
717
729
|
submittedPayloadFingerprintRef.current = fingerprint;
|
|
718
730
|
deliver(payload);
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
if (
|
|
731
|
+
// Always record the submitted text so ↑/↓ can recall it. The full expanded
|
|
732
|
+
// text is persisted (content is never lost); on recall, long text is
|
|
733
|
+
// re-collapsed into a paste marker that still carries its full content.
|
|
734
|
+
if (expandedText.trim().length > 0 || attachments.length > 0) {
|
|
723
735
|
const historyEntry = {
|
|
724
736
|
text: expandedText,
|
|
725
737
|
images: attachments,
|