@gajae-code/ai 0.4.2 → 0.4.3

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.
@@ -717,6 +717,14 @@ export interface Model<TApi extends Api = any> {
717
717
  baseUrl: string;
718
718
  reasoning: boolean;
719
719
  input: ("text" | "image")[];
720
+ /**
721
+ * Output modalities the model can produce. Defaults to text-only when
722
+ * unset. A model that lists `"image"` advertises image-generation support
723
+ * (e.g. an OpenAI-compatible `gpt-image` model behind a proxy), which the
724
+ * `generate_image` tool uses to route requests without first-party
725
+ * provider/id heuristics.
726
+ */
727
+ output?: ("text" | "image")[];
720
728
  cost: {
721
729
  input: number;
722
730
  output: number;
@@ -13,6 +13,10 @@ export type CapturedHttpErrorResponse = {
13
13
  bodyText?: string;
14
14
  bodyJson?: unknown;
15
15
  };
16
+ /** Whether `message` (from a 400 response) signals an unavailable/unknown model. */
17
+ export declare function isModelUnavailableError(message: string, error: unknown): boolean;
18
+ /** Actionable guidance for selecting an available model/provider. */
19
+ export declare function formatModelUnavailableGuidance(dump: RawHttpRequestDump | undefined): string;
16
20
  export declare function appendRawHttpRequestDumpFor400(message: string, error: unknown, dump: RawHttpRequestDump | undefined): Promise<string>;
17
21
  export declare function finalizeErrorMessage(error: unknown, rawRequestDump: RawHttpRequestDump | undefined, capturedErrorResponse?: CapturedHttpErrorResponse): Promise<string>;
18
22
  export declare function withHttpStatus(error: unknown, status: number): Error;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.4.2",
4
+ "version": "0.4.3",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "^0.94.0",
45
45
  "@bufbuild/protobuf": "^2.12.0",
46
- "@gajae-code/utils": "0.4.2",
46
+ "@gajae-code/utils": "0.4.3",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
package/src/models.json CHANGED
@@ -9853,7 +9853,8 @@
9853
9853
  "baseUrl": "https://api.kilo.ai/api/gateway",
9854
9854
  "reasoning": false,
9855
9855
  "input": [
9856
- "text"
9856
+ "text",
9857
+ "image"
9857
9858
  ],
9858
9859
  "cost": {
9859
9860
  "input": 0,
@@ -9872,7 +9873,8 @@
9872
9873
  "baseUrl": "https://api.kilo.ai/api/gateway",
9873
9874
  "reasoning": false,
9874
9875
  "input": [
9875
- "text"
9876
+ "text",
9877
+ "image"
9876
9878
  ],
9877
9879
  "cost": {
9878
9880
  "input": 0,
@@ -16768,7 +16770,8 @@
16768
16770
  "baseUrl": "https://api.kilo.ai/api/gateway",
16769
16771
  "reasoning": false,
16770
16772
  "input": [
16771
- "text"
16773
+ "text",
16774
+ "image"
16772
16775
  ],
16773
16776
  "cost": {
16774
16777
  "input": 0,
@@ -63601,7 +63604,8 @@
63601
63604
  "baseUrl": "https://api.venice.ai/api/v1",
63602
63605
  "reasoning": false,
63603
63606
  "input": [
63604
- "text"
63607
+ "text",
63608
+ "image"
63605
63609
  ],
63606
63610
  "cost": {
63607
63611
  "input": 0,
package/src/types.ts CHANGED
@@ -854,6 +854,14 @@ export interface Model<TApi extends Api = any> {
854
854
  baseUrl: string;
855
855
  reasoning: boolean;
856
856
  input: ("text" | "image")[];
857
+ /**
858
+ * Output modalities the model can produce. Defaults to text-only when
859
+ * unset. A model that lists `"image"` advertises image-generation support
860
+ * (e.g. an OpenAI-compatible `gpt-image` model behind a proxy), which the
861
+ * `generate_image` tool uses to route requests without first-party
862
+ * provider/id heuristics.
863
+ */
864
+ output?: ("text" | "image")[];
857
865
  cost: {
858
866
  input: number; // $/million tokens
859
867
  output: number; // $/million tokens
@@ -1,5 +1,5 @@
1
1
  import * as path from "node:path";
2
- import { extractHttpStatusFromError, getLogsDir } from "@gajae-code/utils";
2
+ import { APP_NAME, extractHttpStatusFromError, getLogsDir } from "@gajae-code/utils";
3
3
  import { isCopilotTransientModelError } from "./retry.js";
4
4
  import { formatErrorMessageWithRetryAfter } from "./retry-after.js";
5
5
 
@@ -26,6 +26,46 @@ type ErrorWithStatus = {
26
26
 
27
27
  const SENSITIVE_HEADERS = ["authorization", "x-api-key", "api-key", "cookie", "set-cookie", "proxy-authorization"];
28
28
 
29
+ /**
30
+ * Privacy note appended next to a saved raw HTTP request dump. The dump is
31
+ * sanitized (secrets/thinking redacted) but can still contain prompt content
32
+ * and request metadata, so we explicitly discourage pasting it into public
33
+ * channels (issue #438).
34
+ */
35
+ const RAW_HTTP_REQUEST_PRIVACY_NOTE =
36
+ "note: this local file is for your own debugging and may contain prompt content or request metadata — review it before sharing and do not paste its contents into public channels (issues, Discord, etc.).";
37
+
38
+ /**
39
+ * Patterns that indicate the configured model is unavailable on the provider
40
+ * (e.g. OpenAI's "The requested model '...' does not exist."). Used to surface
41
+ * actionable model/provider guidance instead of only a raw 400 + log path.
42
+ */
43
+ const MODEL_UNAVAILABLE_PATTERNS: readonly RegExp[] = [
44
+ /\bmodel\b[^\n]*\bdoes not exist\b/i,
45
+ /\bdoes not exist\b[^\n]*\bmodel\b/i,
46
+ /\bmodel\b[^\n]*\b(not found|unavailable|not supported|no access|does not have access)\b/i,
47
+ /\b(unknown|unsupported|invalid)\s+model\b/i,
48
+ ];
49
+
50
+ /** Whether `message` (from a 400 response) signals an unavailable/unknown model. */
51
+ export function isModelUnavailableError(message: string, error: unknown): boolean {
52
+ if (extractHttpStatusFromError(error) !== 400) return false;
53
+ return MODEL_UNAVAILABLE_PATTERNS.some(pattern => pattern.test(message));
54
+ }
55
+
56
+ /** Actionable guidance for selecting an available model/provider. */
57
+ export function formatModelUnavailableGuidance(dump: RawHttpRequestDump | undefined): string {
58
+ const modelPart = dump?.model ? ` '${dump.model}'` : "";
59
+ const providerPart = dump?.provider ? ` on provider '${dump.provider}'` : "";
60
+ return [
61
+ `The configured model${modelPart}${providerPart} is not available on this account/provider.`,
62
+ "Pick an available model before retrying:",
63
+ ` • List available models: ${APP_NAME} --list-models`,
64
+ ` • Run with a model: ${APP_NAME} --model <model>`,
65
+ ` • Configure a provider: ${APP_NAME} setup provider`,
66
+ ].join("\n");
67
+ }
68
+
29
69
  export async function appendRawHttpRequestDumpFor400(
30
70
  message: string,
31
71
  error: unknown,
@@ -41,7 +81,7 @@ export async function appendRawHttpRequestDumpFor400(
41
81
 
42
82
  try {
43
83
  await Bun.write(filePath, `${JSON.stringify(sanitizedDump, null, 2)}\n`);
44
- return `${message}\nraw-http-request=${filePath}`;
84
+ return `${message}\nraw-http-request=${filePath}\n${RAW_HTTP_REQUEST_PRIVACY_NOTE}`;
45
85
  } catch (writeError) {
46
86
  const writeMessage = writeError instanceof Error ? writeError.message : String(writeError);
47
87
  return `${message}\nraw-http-request-save-failed=${writeMessage}`;
@@ -62,6 +102,9 @@ export async function finalizeErrorMessage(
62
102
  message = `${message}\n${capturedMessage}`;
63
103
  }
64
104
  }
105
+ if (isModelUnavailableError(message, error)) {
106
+ message = `${message}\n\n${formatModelUnavailableGuidance(rawRequestDump)}`;
107
+ }
65
108
  return appendRawHttpRequestDumpFor400(message, error, rawRequestDump);
66
109
  }
67
110