opencode-quotas 0.0.1
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/README.md +344 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +42 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/constants.d.ts +9 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +15 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/defaults.d.ts +3 -0
- package/dist/src/defaults.d.ts.map +1 -0
- package/dist/src/defaults.js +52 -0
- package/dist/src/defaults.js.map +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +265 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interfaces.d.ts +197 -0
- package/dist/src/interfaces.d.ts.map +1 -0
- package/dist/src/interfaces.js +2 -0
- package/dist/src/interfaces.js.map +1 -0
- package/dist/src/logger.d.ts +14 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +44 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/plugin-state.d.ts +20 -0
- package/dist/src/plugin-state.d.ts.map +1 -0
- package/dist/src/plugin-state.js +53 -0
- package/dist/src/plugin-state.js.map +1 -0
- package/dist/src/providers/antigravity/auth.d.ts +12 -0
- package/dist/src/providers/antigravity/auth.d.ts.map +1 -0
- package/dist/src/providers/antigravity/auth.js +109 -0
- package/dist/src/providers/antigravity/auth.js.map +1 -0
- package/dist/src/providers/antigravity/index.d.ts +2 -0
- package/dist/src/providers/antigravity/index.d.ts.map +1 -0
- package/dist/src/providers/antigravity/index.js +2 -0
- package/dist/src/providers/antigravity/index.js.map +1 -0
- package/dist/src/providers/antigravity/provider.d.ts +34 -0
- package/dist/src/providers/antigravity/provider.d.ts.map +1 -0
- package/dist/src/providers/antigravity/provider.js +216 -0
- package/dist/src/providers/antigravity/provider.js.map +1 -0
- package/dist/src/providers/codex.d.ts +4 -0
- package/dist/src/providers/codex.d.ts.map +1 -0
- package/dist/src/providers/codex.js +242 -0
- package/dist/src/providers/codex.js.map +1 -0
- package/dist/src/providers/github.d.ts +4 -0
- package/dist/src/providers/github.d.ts.map +1 -0
- package/dist/src/providers/github.js +139 -0
- package/dist/src/providers/github.js.map +1 -0
- package/dist/src/quota-cache.d.ts +26 -0
- package/dist/src/quota-cache.d.ts.map +1 -0
- package/dist/src/quota-cache.js +107 -0
- package/dist/src/quota-cache.js.map +1 -0
- package/dist/src/registry.d.ts +3 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +23 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/services/aggregation-service.d.ts +34 -0
- package/dist/src/services/aggregation-service.d.ts.map +1 -0
- package/dist/src/services/aggregation-service.js +89 -0
- package/dist/src/services/aggregation-service.js.map +1 -0
- package/dist/src/services/config-loader.d.ts +25 -0
- package/dist/src/services/config-loader.d.ts.map +1 -0
- package/dist/src/services/config-loader.js +105 -0
- package/dist/src/services/config-loader.js.map +1 -0
- package/dist/src/services/history-service.d.ts +15 -0
- package/dist/src/services/history-service.d.ts.map +1 -0
- package/dist/src/services/history-service.js +99 -0
- package/dist/src/services/history-service.js.map +1 -0
- package/dist/src/services/prediction-engine.d.ts +47 -0
- package/dist/src/services/prediction-engine.d.ts.map +1 -0
- package/dist/src/services/prediction-engine.js +94 -0
- package/dist/src/services/prediction-engine.js.map +1 -0
- package/dist/src/services/quota-service.d.ts +41 -0
- package/dist/src/services/quota-service.d.ts.map +1 -0
- package/dist/src/services/quota-service.js +257 -0
- package/dist/src/services/quota-service.js.map +1 -0
- package/dist/src/tools/quotas.d.ts +11 -0
- package/dist/src/tools/quotas.d.ts.map +1 -0
- package/dist/src/tools/quotas.js +62 -0
- package/dist/src/tools/quotas.js.map +1 -0
- package/dist/src/ui/progress-bar.d.ts +20 -0
- package/dist/src/ui/progress-bar.d.ts.map +1 -0
- package/dist/src/ui/progress-bar.js +150 -0
- package/dist/src/ui/progress-bar.js.map +1 -0
- package/dist/src/ui/quota-table.d.ts +15 -0
- package/dist/src/ui/quota-table.d.ts.map +1 -0
- package/dist/src/ui/quota-table.js +136 -0
- package/dist/src/ui/quota-table.js.map +1 -0
- package/dist/src/utils/debug.d.ts +1 -0
- package/dist/src/utils/debug.d.ts.map +1 -0
- package/dist/src/utils/debug.js +3 -0
- package/dist/src/utils/debug.js.map +1 -0
- package/dist/src/utils/paths.d.ts +7 -0
- package/dist/src/utils/paths.d.ts.map +1 -0
- package/dist/src/utils/paths.js +37 -0
- package/dist/src/utils/paths.js.map +1 -0
- package/dist/src/utils/time.d.ts +3 -0
- package/dist/src/utils/time.d.ts.map +1 -0
- package/dist/src/utils/time.js +38 -0
- package/dist/src/utils/time.js.map +1 -0
- package/dist/src/utils/validation.d.ts +6 -0
- package/dist/src/utils/validation.d.ts.map +1 -0
- package/dist/src/utils/validation.js +66 -0
- package/dist/src/utils/validation.js.map +1 -0
- package/package.json +42 -0
- package/src/cli.ts +53 -0
- package/src/constants.ts +17 -0
- package/src/defaults.ts +53 -0
- package/src/index.ts +338 -0
- package/src/interfaces.ts +258 -0
- package/src/logger.ts +55 -0
- package/src/plugin-state.ts +65 -0
- package/src/providers/antigravity/auth.ts +163 -0
- package/src/providers/antigravity/index.ts +1 -0
- package/src/providers/antigravity/provider.ts +337 -0
- package/src/providers/codex.ts +327 -0
- package/src/providers/github.ts +161 -0
- package/src/quota-cache.ts +157 -0
- package/src/registry.ts +30 -0
- package/src/services/aggregation-service.ts +116 -0
- package/src/services/config-loader.ts +124 -0
- package/src/services/history-service.ts +116 -0
- package/src/services/prediction-engine.ts +133 -0
- package/src/services/quota-service.ts +343 -0
- package/src/tools/quotas.ts +78 -0
- package/src/ui/progress-bar.ts +204 -0
- package/src/ui/quota-table.ts +173 -0
- package/src/utils/debug.ts +1 -0
- package/src/utils/paths.ts +41 -0
- package/src/utils/time.ts +40 -0
- package/src/utils/validation.ts +63 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-quotas",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Opencode quota hub plugin for Antigravity and Codex",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"packageManager": "bun@1.1.20",
|
|
15
|
+
"bin": {
|
|
16
|
+
"opencode-quotas": "./dist/src/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"opencode-quotas": "bun src/cli.ts",
|
|
26
|
+
"build": "tsc -p tsconfig.build.json",
|
|
27
|
+
"typecheck": "tsc --noEmit"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@ai-sdk/provider": "^3.0.2",
|
|
34
|
+
"@opencode-ai/plugin": "^1.1.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"js-yaml": "^4.1.1",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"yaml": "^2.8.2"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { QuotaService } from "./services/quota-service";
|
|
3
|
+
import { HistoryService } from "./services/history-service";
|
|
4
|
+
import { renderQuotaTable } from "./ui/quota-table";
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
if (process.argv.includes("--no-color")) {
|
|
8
|
+
process.env.OPENCODE_QUOTAS_NO_COLOR = "1";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const historyService = new HistoryService();
|
|
12
|
+
await historyService.init();
|
|
13
|
+
|
|
14
|
+
const quotaService = new QuotaService();
|
|
15
|
+
await quotaService.init(process.cwd(), historyService);
|
|
16
|
+
|
|
17
|
+
const config = quotaService.getConfig();
|
|
18
|
+
|
|
19
|
+
// Parse arguments for provider and model filtering
|
|
20
|
+
let providerId: string | undefined;
|
|
21
|
+
let modelId: string | undefined;
|
|
22
|
+
|
|
23
|
+
const providerIdx = process.argv.indexOf("--provider");
|
|
24
|
+
if (providerIdx !== -1 && providerIdx + 1 < process.argv.length) {
|
|
25
|
+
providerId = process.argv[providerIdx + 1];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const modelIdx = process.argv.indexOf("--model");
|
|
29
|
+
if (modelIdx !== -1 && modelIdx + 1 < process.argv.length) {
|
|
30
|
+
modelId = process.argv[modelIdx + 1];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const filteredResults = await quotaService.getQuotas({ providerId, modelId });
|
|
34
|
+
|
|
35
|
+
if (filteredResults.length === 0) {
|
|
36
|
+
console.log("No active quotas found.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(""); // Empty line
|
|
41
|
+
console.log("📊 OpenCode Quotas");
|
|
42
|
+
console.log("------------------");
|
|
43
|
+
|
|
44
|
+
renderQuotaTable(filteredResults, {
|
|
45
|
+
progressBarConfig: config.progressBar,
|
|
46
|
+
tableConfig: config.table,
|
|
47
|
+
}).forEach((row) => {
|
|
48
|
+
console.log(row.line);
|
|
49
|
+
});
|
|
50
|
+
console.log(""); // Empty line
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
main().catch(console.error);
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const PLUGIN_FOOTER_SIGNATURE = "_Opencode Quotas";
|
|
2
|
+
|
|
3
|
+
// Heuristic patterns for detecting reasoning/thinking blocks
|
|
4
|
+
export const REASONING_PATTERNS = [
|
|
5
|
+
/^<thinking>/i,
|
|
6
|
+
/^<antThinking>/i,
|
|
7
|
+
/^(Thinking|Reasoning|Analysis):\s*(\n|$)/i
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
// File paths
|
|
11
|
+
export const DEBUG_LOG_FILENAME = "quotas-debug.log";
|
|
12
|
+
|
|
13
|
+
export const SKIP_REASONS = {
|
|
14
|
+
REASONING: "skip:reasoning",
|
|
15
|
+
SUBAGENT: "skip:subagent",
|
|
16
|
+
FOOTER_PRESENT: "skip:footer_present",
|
|
17
|
+
};
|
package/src/defaults.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type QuotaConfig } from "./interfaces";
|
|
2
|
+
|
|
3
|
+
// Default configuration for quota rendering and grouping
|
|
4
|
+
export const DEFAULT_CONFIG: QuotaConfig = {
|
|
5
|
+
displayMode: "simple",
|
|
6
|
+
footer: true,
|
|
7
|
+
showFooterTitle: true,
|
|
8
|
+
debug: false,
|
|
9
|
+
progressBar: {
|
|
10
|
+
color: false,
|
|
11
|
+
gradients: [
|
|
12
|
+
{ threshold: 0.5, color: "green" },
|
|
13
|
+
{ threshold: 0.8, color: "yellow" },
|
|
14
|
+
{ threshold: 1.0, color: "red" },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
table: {
|
|
18
|
+
header: true,
|
|
19
|
+
},
|
|
20
|
+
filterByCurrentModel: false,
|
|
21
|
+
showUnaggregated: false,
|
|
22
|
+
aggregatedGroups: [
|
|
23
|
+
{
|
|
24
|
+
id: "ag-flash",
|
|
25
|
+
name: "Antigravity Flash",
|
|
26
|
+
patterns: ["flash"],
|
|
27
|
+
providerId: "antigravity",
|
|
28
|
+
strategy: "most_critical",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "ag-pro",
|
|
32
|
+
name: "Antigravity Pro",
|
|
33
|
+
patterns: ["pro", "gemini"],
|
|
34
|
+
providerId: "antigravity",
|
|
35
|
+
strategy: "most_critical",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "ag-premium",
|
|
39
|
+
name: "Antigravity Premium",
|
|
40
|
+
patterns: ["claude", "gpt", "o1"],
|
|
41
|
+
providerId: "antigravity",
|
|
42
|
+
strategy: "most_critical",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "codex-smart",
|
|
46
|
+
name: "Codex Usage",
|
|
47
|
+
sources: ["codex-primary", "codex-secondary"],
|
|
48
|
+
strategy: "most_critical",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
historyMaxAgeHours: 24,
|
|
52
|
+
pollingInterval: 60_000,
|
|
53
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { type Plugin, type Hooks } from "@opencode-ai/plugin";
|
|
2
|
+
import { type AssistantMessage, type UserMessage } from "@opencode-ai/sdk";
|
|
3
|
+
import { QuotaService } from "./services/quota-service";
|
|
4
|
+
import { HistoryService } from "./services/history-service";
|
|
5
|
+
import { renderQuotaTable } from "./ui/quota-table";
|
|
6
|
+
import { type QuotaData } from "./interfaces";
|
|
7
|
+
import { QuotaCache } from "./quota-cache";
|
|
8
|
+
import {
|
|
9
|
+
PLUGIN_FOOTER_SIGNATURE,
|
|
10
|
+
REASONING_PATTERNS,
|
|
11
|
+
SKIP_REASONS,
|
|
12
|
+
} from "./constants";
|
|
13
|
+
import { logger } from "./logger";
|
|
14
|
+
import { getPluginState } from "./plugin-state";
|
|
15
|
+
import { createQuotaTool } from "./tools/quotas";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extended message type with additional fields that may be present at runtime.
|
|
19
|
+
* Uses Omit to override required fields that we know may be optional.
|
|
20
|
+
*/
|
|
21
|
+
type ExtendedAssistantMessage = Omit<AssistantMessage, "parentID"> & {
|
|
22
|
+
type?: string;
|
|
23
|
+
mode?: string;
|
|
24
|
+
parentID?: string;
|
|
25
|
+
modelID?: string;
|
|
26
|
+
providerID?: string;
|
|
27
|
+
tokens?: {
|
|
28
|
+
input?: number;
|
|
29
|
+
output?: number;
|
|
30
|
+
reasoning?: number;
|
|
31
|
+
cache?: { read?: number; write?: number };
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* QuotaHub Plugin for OpenCode.ai
|
|
37
|
+
*/
|
|
38
|
+
export const QuotaHubPlugin: Plugin = async ({ client, $, directory }) => {
|
|
39
|
+
const state = getPluginState();
|
|
40
|
+
const historyService = new HistoryService();
|
|
41
|
+
const quotaService = new QuotaService();
|
|
42
|
+
|
|
43
|
+
let quotaCache: QuotaCache | undefined;
|
|
44
|
+
let initPromise: Promise<void> | undefined;
|
|
45
|
+
|
|
46
|
+
// Dedicated initialization function
|
|
47
|
+
const ensureInit = async (): Promise<void> => {
|
|
48
|
+
if (initPromise) return initPromise;
|
|
49
|
+
|
|
50
|
+
initPromise = (async () => {
|
|
51
|
+
try {
|
|
52
|
+
await historyService.init();
|
|
53
|
+
await quotaService.init(directory, historyService);
|
|
54
|
+
|
|
55
|
+
const config = quotaService.getConfig();
|
|
56
|
+
const providers = quotaService.getProviders();
|
|
57
|
+
logger.debug("init:providers", {
|
|
58
|
+
ids: providers.map((p) => p.id),
|
|
59
|
+
count: providers.length,
|
|
60
|
+
});
|
|
61
|
+
quotaCache = new QuotaCache(providers, {
|
|
62
|
+
refreshIntervalMs: config.pollingInterval ?? 60_000,
|
|
63
|
+
historyService,
|
|
64
|
+
debug: !!config.debug,
|
|
65
|
+
});
|
|
66
|
+
quotaCache.start();
|
|
67
|
+
logger.debug("init:complete");
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error("Failed to initialize QuotaHubPlugin:", e);
|
|
70
|
+
// Keep the promise but it failed. Future calls will see it as failed.
|
|
71
|
+
throw e;
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
|
|
75
|
+
return initPromise;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Trigger background initialization
|
|
79
|
+
ensureInit().catch(() => {});
|
|
80
|
+
|
|
81
|
+
const hooks: Hooks = {
|
|
82
|
+
/**
|
|
83
|
+
* The platform calls this hook after a text generation is complete.
|
|
84
|
+
* We use it to append quota information to the end of the final assistant message.
|
|
85
|
+
*/
|
|
86
|
+
"experimental.text.complete": async (
|
|
87
|
+
input: {
|
|
88
|
+
sessionID: string;
|
|
89
|
+
messageID: string;
|
|
90
|
+
partID: string;
|
|
91
|
+
},
|
|
92
|
+
output: {
|
|
93
|
+
text: string;
|
|
94
|
+
},
|
|
95
|
+
): Promise<void> => {
|
|
96
|
+
// Ensure initialization is complete before processing
|
|
97
|
+
await ensureInit().catch((e) => {
|
|
98
|
+
logger.error("init:error", { error: e });
|
|
99
|
+
});
|
|
100
|
+
if (!quotaCache) {
|
|
101
|
+
const config = quotaService.getConfig();
|
|
102
|
+
if (config.debug)
|
|
103
|
+
logger.debug("hook:no_cache", {
|
|
104
|
+
messageID: input.messageID,
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cache = quotaCache;
|
|
110
|
+
const config = quotaService.getConfig();
|
|
111
|
+
const debugLog = (msg: string, data?: any) => {
|
|
112
|
+
if (config.debug) logger.debug(msg, data);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (config.footer === false) {
|
|
116
|
+
debugLog("skip:footer_disabled", {
|
|
117
|
+
messageID: input.messageID,
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Log hook invocation
|
|
123
|
+
debugLog("hook:experimental.text.complete", {
|
|
124
|
+
input,
|
|
125
|
+
processed: state.isProcessed(input.messageID),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Fast path check
|
|
129
|
+
if (state.isProcessed(input.messageID)) {
|
|
130
|
+
debugLog("skip:already_processed", {
|
|
131
|
+
messageID: input.messageID,
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Secondary safeguard: check if footer already present
|
|
137
|
+
if (output.text.includes(PLUGIN_FOOTER_SIGNATURE)) {
|
|
138
|
+
debugLog(SKIP_REASONS.FOOTER_PRESENT, {
|
|
139
|
+
messageID: input.messageID,
|
|
140
|
+
});
|
|
141
|
+
state.markProcessed(input.messageID);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
debugLog("lock:acquire_start", { messageID: input.messageID });
|
|
146
|
+
// Acquire lock for this message
|
|
147
|
+
const release = await state.acquireLock(input.messageID);
|
|
148
|
+
debugLog("lock:acquired", { messageID: input.messageID });
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// After acquiring lock, re-check if processed
|
|
152
|
+
if (state.isProcessed(input.messageID)) {
|
|
153
|
+
debugLog("skip:already_processed_after_lock", {
|
|
154
|
+
messageID: input.messageID,
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Double-check text content in case another process injected it while we waited for lock
|
|
160
|
+
if (output.text.includes(PLUGIN_FOOTER_SIGNATURE)) {
|
|
161
|
+
debugLog("skip:footer_present_after_lock", {
|
|
162
|
+
messageID: input.messageID,
|
|
163
|
+
});
|
|
164
|
+
state.markProcessed(input.messageID);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Fetch message to check role
|
|
169
|
+
const { data: result } = await client.session.message({
|
|
170
|
+
path: {
|
|
171
|
+
id: input.sessionID,
|
|
172
|
+
messageID: input.messageID,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!result || result.info.role !== "assistant") {
|
|
177
|
+
debugLog("skip:not_assistant", {
|
|
178
|
+
messageID: input.messageID,
|
|
179
|
+
role: result?.info?.role,
|
|
180
|
+
});
|
|
181
|
+
state.markProcessed(input.messageID);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const assistantMsg = result.info as ExtendedAssistantMessage;
|
|
186
|
+
|
|
187
|
+
// Mark as processed as soon as we've identified it's an assistant message
|
|
188
|
+
// We do this before the quota check to avoid race conditions if no quotas are found.
|
|
189
|
+
state.markProcessed(input.messageID);
|
|
190
|
+
|
|
191
|
+
// Log message details
|
|
192
|
+
debugLog("message:details", {
|
|
193
|
+
id: input.messageID,
|
|
194
|
+
mode: assistantMsg.mode,
|
|
195
|
+
tokens: assistantMsg.tokens,
|
|
196
|
+
type: assistantMsg.type,
|
|
197
|
+
modelID: assistantMsg.modelID,
|
|
198
|
+
providerID: assistantMsg.providerID,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Skip if it's a subagent mode (thinking step), unless it's a whitelisted agent (plan/build)
|
|
202
|
+
if (assistantMsg.mode === "subagent") {
|
|
203
|
+
let allowed = false;
|
|
204
|
+
if (assistantMsg.parentID) {
|
|
205
|
+
try {
|
|
206
|
+
const { data: parentResult } =
|
|
207
|
+
await client.session.message({
|
|
208
|
+
path: {
|
|
209
|
+
id: input.sessionID,
|
|
210
|
+
messageID: assistantMsg.parentID,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (parentResult?.info?.role === "user") {
|
|
215
|
+
const userMsg =
|
|
216
|
+
parentResult.info as UserMessage;
|
|
217
|
+
// Allow plan and build agents even in subagent mode
|
|
218
|
+
if (["plan", "build"].includes(userMsg.agent)) {
|
|
219
|
+
allowed = true;
|
|
220
|
+
debugLog("allow:subagent_exception", {
|
|
221
|
+
agent: userMsg.agent,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
debugLog("error:check_parent_agent", e);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!allowed) {
|
|
231
|
+
debugLog(SKIP_REASONS.SUBAGENT);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Skip reasoning messages (explicit mode or type)
|
|
237
|
+
if (
|
|
238
|
+
assistantMsg.mode === "reasoning" ||
|
|
239
|
+
assistantMsg.type === "reasoning"
|
|
240
|
+
) {
|
|
241
|
+
debugLog(SKIP_REASONS.REASONING);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Skip if it appears to be a reasoning-only message based on tokens
|
|
246
|
+
const reasoningTokens = assistantMsg.tokens?.reasoning ?? 0;
|
|
247
|
+
const outputTokens = assistantMsg.tokens?.output ?? 0;
|
|
248
|
+
if (
|
|
249
|
+
assistantMsg.tokens &&
|
|
250
|
+
reasoningTokens > 0 &&
|
|
251
|
+
(outputTokens === 0 || outputTokens === reasoningTokens)
|
|
252
|
+
) {
|
|
253
|
+
debugLog(SKIP_REASONS.REASONING, assistantMsg.tokens);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Heuristic: Check if text starts with "Thinking:" or similar
|
|
258
|
+
const trimmedText = output.text.trim();
|
|
259
|
+
for (const pattern of REASONING_PATTERNS) {
|
|
260
|
+
if (pattern.test(trimmedText)) {
|
|
261
|
+
debugLog(SKIP_REASONS.REASONING, {
|
|
262
|
+
pattern: pattern.toString(),
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const snapshot = cache.getSnapshot();
|
|
269
|
+
const rawResults: QuotaData[] = snapshot.data;
|
|
270
|
+
debugLog("cache:snapshot", {
|
|
271
|
+
fetchedAt: snapshot.fetchedAt?.toISOString(),
|
|
272
|
+
totalCount: rawResults.length,
|
|
273
|
+
hasError: !!snapshot.lastError,
|
|
274
|
+
});
|
|
275
|
+
if (rawResults.length === 0) {
|
|
276
|
+
debugLog("skip:no_cached_quotas", {
|
|
277
|
+
fetchedAt: snapshot.fetchedAt?.toISOString(),
|
|
278
|
+
lastError: snapshot.lastError,
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Process (filter, sort) using the shared service
|
|
284
|
+
const filteredResults = quotaService.processQuotas(rawResults, {
|
|
285
|
+
providerId: assistantMsg.providerID,
|
|
286
|
+
modelId: assistantMsg.modelID,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
debugLog("quotas:processed", {
|
|
290
|
+
before: rawResults.length,
|
|
291
|
+
after: filteredResults.length,
|
|
292
|
+
providerId: assistantMsg.providerID,
|
|
293
|
+
modelId: assistantMsg.modelID,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (filteredResults.length === 0) {
|
|
297
|
+
debugLog("skip:all_quotas_filtered", {
|
|
298
|
+
providerId: assistantMsg.providerID,
|
|
299
|
+
modelId: assistantMsg.modelID,
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const lines = renderQuotaTable(filteredResults, {
|
|
305
|
+
progressBarConfig: config.progressBar,
|
|
306
|
+
tableConfig: config.table,
|
|
307
|
+
}).map((l) => l.line);
|
|
308
|
+
|
|
309
|
+
// Append to message text
|
|
310
|
+
const showMode = config.progressBar?.show ?? "used";
|
|
311
|
+
const modeLabel =
|
|
312
|
+
showMode === "available" ? "(Remaining)" : "(Used)";
|
|
313
|
+
// Build visible header only if enabled in config
|
|
314
|
+
const showTitle = config.showFooterTitle !== false;
|
|
315
|
+
const titleText = showTitle
|
|
316
|
+
? `${PLUGIN_FOOTER_SIGNATURE} ${modeLabel}_\n`
|
|
317
|
+
: "";
|
|
318
|
+
|
|
319
|
+
// Append table lines (no invisible marker)
|
|
320
|
+
output.text += "\n\n" + titleText + lines.join("\n");
|
|
321
|
+
debugLog("inject:footer", {
|
|
322
|
+
messageID: input.messageID,
|
|
323
|
+
lines: lines.length,
|
|
324
|
+
});
|
|
325
|
+
} finally {
|
|
326
|
+
debugLog("lock:release", { messageID: input.messageID });
|
|
327
|
+
release();
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Create the quota tool that can be called by the LLM
|
|
333
|
+
const quotaTool = createQuotaTool(quotaService, () =>
|
|
334
|
+
quotaService.getConfig(),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
return hooks;
|
|
338
|
+
};
|