@gentbajko/slopify 2.0.2 → 2.1.0
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/SUBTITLES.md +1 -1
- package/dist/adapters/alignment/protocol.js +11 -1
- package/dist/adapters/alignment/runner.js +4 -1
- package/dist/adapters/alignment/threads.js +11 -0
- package/dist/adapters/alignment/worker.js +66 -9
- package/dist/adapters/explain.js +93 -0
- package/dist/adapters/ffmpeg.js +3 -3
- package/dist/adapters/host-cli/index.js +34 -3
- package/dist/adapters/host-cli/transport.js +4 -4
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +12 -12
- package/dist/adapters/image/codex.js +24 -8
- package/dist/adapters/image/fal.js +12 -6
- package/dist/adapters/image/google.js +18 -8
- package/dist/adapters/image/models.js +8 -2
- package/dist/adapters/image/openai.js +15 -6
- package/dist/adapters/image/replicate.js +16 -7
- package/dist/adapters/llm/claude-code-models.js +1 -1
- package/dist/adapters/llm/claude-code.js +16 -7
- package/dist/adapters/llm/cli-login-error.js +2 -1
- package/dist/adapters/llm/codex-models.js +1 -1
- package/dist/adapters/llm/codex.js +5 -6
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-models.js +1 -1
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +15 -11
- package/dist/adapters/llm/openrouter.js +21 -9
- package/dist/adapters/llm/run-cli.js +14 -11
- package/dist/adapters/tts/cartesia.js +10 -3
- package/dist/adapters/tts/elevenlabs.js +18 -6
- package/dist/adapters/tts/inworld-async.js +47 -16
- package/dist/adapters/tts/inworld.js +25 -15
- package/dist/adapters/tts/openai.js +18 -6
- package/dist/assets/document/SOURCE.txt +7 -0
- package/dist/assets/document/background.jpg +0 -0
- package/dist/assets/document/fonts/Cinzel-Black.ttf +0 -0
- package/dist/assets/document/fonts/Cinzel-Bold.ttf +0 -0
- package/dist/assets/document/fonts/Cinzel-Medium.ttf +0 -0
- package/dist/assets/document/fonts/Cinzel-Regular.ttf +0 -0
- package/dist/assets/document/fonts/OFL.txt +93 -0
- package/dist/assets/document/fonts/SOURCE.txt +11 -0
- package/dist/catalog/registry.js +13 -10
- package/dist/catalog/store.js +1 -1
- package/dist/catalog/validate.js +16 -6
- package/dist/edge/cli.js +33 -7
- package/dist/edge/docker-launch.js +6 -6
- package/dist/edge/docker-projects/engine.js +34 -13
- package/dist/edge/docker-projects/install.js +8 -8
- package/dist/edge/docker-projects/state.js +11 -7
- package/dist/edge/docker-projects/tree.js +6 -6
- package/dist/edge/docker.js +13 -13
- package/dist/edge/host-cli.js +14 -0
- package/dist/edge/http/actions.js +27 -25
- package/dist/edge/http/app.js +3 -3
- package/dist/edge/http/audio-preview.js +1 -1
- package/dist/edge/http/checkpoints.js +5 -1
- package/dist/edge/http/diagnostics.js +3 -1
- package/dist/edge/http/draft-files.js +2 -2
- package/dist/edge/http/draft-problem.js +11 -1
- package/dist/edge/http/files.js +4 -4
- package/dist/edge/http/folder-location-schema.js +5 -8
- package/dist/edge/http/folder-location.js +13 -8
- package/dist/edge/http/fonts.js +22 -10
- package/dist/edge/http/host-cli.js +61 -14
- package/dist/edge/http/multipart.js +7 -5
- package/dist/edge/http/open-folder.js +2 -2
- package/dist/edge/http/planning.js +4 -4
- package/dist/edge/http/problem.js +2 -2
- package/dist/edge/http/project-templates.js +7 -3
- package/dist/edge/http/projects.js +8 -8
- package/dist/edge/http/prompts.js +9 -4
- package/dist/edge/http/providers.js +15 -7
- package/dist/edge/http/revision-files.js +8 -4
- package/dist/edge/http/revisions.js +30 -4
- package/dist/edge/http/schedules.js +9 -9
- package/dist/edge/http/settings.js +15 -9
- package/dist/edge/http/staging.js +5 -5
- package/dist/edge/http/storage.js +4 -4
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/tutorial.js +2 -2
- package/dist/edge/http/update.js +3 -3
- package/dist/host-cli/install.js +28 -6
- package/dist/host-cli/open-folder.js +95 -0
- package/dist/host-cli/paths.js +6 -6
- package/dist/host-cli/runtime.js +2 -0
- package/dist/host-cli/server.js +1 -1
- package/dist/host-cli/service.js +11 -11
- package/dist/host-cli/status.js +7 -3
- package/dist/kernel/config/index.js +6 -4
- package/dist/kernel/db/migrate.js +27 -2
- package/dist/kernel/db/migrations/0013-schedule-topic-queue.sql +4 -0
- package/dist/kernel/db/migrations/0014-document-stage.sql +44 -0
- package/dist/kernel/db/migrations/0015-description-prompts.sql +13 -0
- package/dist/kernel/lock.js +4 -3
- package/dist/kernel/pipeline.js +11 -1
- package/dist/kernel/ports/host-cli.js +14 -0
- package/dist/kernel/ports/subtitles.js +15 -1
- package/dist/kernel/runner/attempt.js +26 -2
- package/dist/kernel/runner/graph.js +6 -1
- package/dist/main.js +8 -4
- package/dist/slices/admission/model.js +7 -1
- package/dist/slices/admission/rules.js +98 -22
- package/dist/slices/admission/schema.js +16 -3
- package/dist/slices/admission/start.js +3 -2
- package/dist/slices/article/continuation.js +4 -2
- package/dist/slices/article/plain.js +2 -2
- package/dist/slices/article/provided-entries.js +3 -3
- package/dist/slices/article/run.js +2 -2
- package/dist/slices/article/segments.js +5 -3
- package/dist/slices/control/providers.js +20 -7
- package/dist/slices/document/blocks.js +129 -0
- package/dist/slices/document/flow.js +117 -0
- package/dist/slices/document/fonts.js +54 -0
- package/dist/slices/document/model.js +11 -0
- package/dist/slices/document/pages.js +198 -0
- package/dist/slices/document/render.js +242 -0
- package/dist/slices/document/sources.js +35 -0
- package/dist/slices/document/theme.js +158 -0
- package/dist/slices/document/writer.js +66 -0
- package/dist/slices/estimate/index.js +14 -2
- package/dist/slices/fonts/catalog.js +1 -1
- package/dist/slices/fonts/upload.js +1 -1
- package/dist/slices/images/run.js +5 -5
- package/dist/slices/library/lint.js +13 -7
- package/dist/slices/library/model.js +1 -1
- package/dist/slices/library/slots.js +6 -3
- package/dist/slices/narration/concat.js +1 -1
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +2 -2
- package/dist/slices/narration/run.js +7 -7
- package/dist/slices/narration/steering.js +3 -3
- package/dist/slices/play-drafts/convert.js +35 -5
- package/dist/slices/play-drafts/readiness.js +15 -8
- package/dist/slices/play-drafts/review-inputs.js +7 -4
- package/dist/slices/play-drafts/review.js +3 -3
- package/dist/slices/play-drafts/schema.js +22 -5
- package/dist/slices/play-drafts/service.js +3 -1
- package/dist/slices/play-drafts/uploads.js +1 -1
- package/dist/slices/project-templates/from-project.js +10 -0
- package/dist/slices/project-templates/setup.js +1 -0
- package/dist/slices/rebuild/admission-repo.js +2 -2
- package/dist/slices/rebuild/preview-details.js +11 -1
- package/dist/slices/rebuild/recipe-audio.js +3 -0
- package/dist/slices/rebuild/recipe-build.js +6 -1
- package/dist/slices/rebuild/recipe-document.js +36 -0
- package/dist/slices/rebuild/recipe-exports.js +14 -2
- package/dist/slices/rebuild/recipe-fixture.js +4 -0
- package/dist/slices/rebuild/recipe-legacy.js +6 -1
- package/dist/slices/rebuild/recipe-model.js +2 -0
- package/dist/slices/rebuild/recipe-narration-text.js +1 -1
- package/dist/slices/rebuild/recipe-provider-choice.js +3 -0
- package/dist/slices/rebuild/recipe-save.js +15 -3
- package/dist/slices/rebuild/recipe-validation.js +15 -2
- package/dist/slices/rebuild/recipe-visual.js +11 -2
- package/dist/slices/rebuild/recipe-work.js +15 -0
- package/dist/slices/rebuild/recipe-youtube.js +33 -0
- package/dist/slices/rebuild/recovery-selection.js +2 -0
- package/dist/slices/rebuild/recovery.js +12 -7
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/runtime-actions.js +2 -0
- package/dist/slices/rebuild/runtime-admission.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-checkpoints.js +4 -4
- package/dist/slices/rebuild/runtime-document.js +85 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +7 -6
- package/dist/slices/rebuild/runtime-export.js +39 -23
- package/dist/slices/rebuild/runtime-local.js +6 -6
- package/dist/slices/rebuild/runtime-narration-text.js +46 -7
- package/dist/slices/rebuild/runtime-piece-label.js +66 -0
- package/dist/slices/rebuild/runtime-provider.js +6 -4
- package/dist/slices/rebuild/runtime-publication.js +4 -3
- package/dist/slices/rebuild/runtime-run.js +22 -4
- package/dist/slices/rebuild/runtime-store.js +7 -2
- package/dist/slices/rebuild/runtime-subtitles.js +82 -16
- package/dist/slices/rebuild/runtime-youtube.js +84 -0
- package/dist/slices/rebuild/transition-repo.js +8 -6
- package/dist/slices/reruns/cascade.js +6 -4
- package/dist/slices/reruns/index.js +1 -1
- package/dist/slices/research/run.js +9 -7
- package/dist/slices/research/synthesis.js +4 -2
- package/dist/slices/revisions/mutation-assets.js +26 -8
- package/dist/slices/revisions/mutation-cues.js +4 -2
- package/dist/slices/revisions/projection.js +2 -1
- package/dist/slices/revisions/rules.js +18 -9
- package/dist/slices/revisions/schema.js +1 -1
- package/dist/slices/schedules/repo.js +55 -11
- package/dist/slices/schedules/scheduler.js +25 -8
- package/dist/slices/schedules/schema.js +19 -4
- package/dist/slices/schedules/service.js +2 -0
- package/dist/slices/settings/cli-paths.js +7 -4
- package/dist/slices/settings/models.js +4 -4
- package/dist/slices/settings/playback.js +2 -2
- package/dist/slices/storage/assets.js +5 -5
- package/dist/slices/storage/downloads.js +1 -0
- package/dist/slices/storage/layout.js +7 -1
- package/dist/slices/storage/model.js +3 -0
- package/dist/slices/subtitles/captions.js +1 -1
- package/dist/slices/subtitles/prepare.js +7 -6
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/telemetry/model.js +1 -1
- package/dist/slices/thumbnail/run.js +8 -8
- package/dist/slices/video/audio-export.js +11 -3
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/edit-list.js +73 -0
- package/dist/slices/video/ffmpeg.js +129 -61
- package/dist/slices/video/motion.js +51 -0
- package/dist/slices/video/plan.js +62 -28
- package/dist/slices/video/reuse-audio.js +2 -0
- package/dist/slices/video/run.js +23 -12
- package/dist/slices/video/slideshow.js +48 -0
- package/dist/slices/video/write-export.js +17 -25
- package/dist/slices/youtube/answer.js +199 -0
- package/dist/slices/youtube/model.js +27 -0
- package/dist/slices/youtube/timestamps.js +24 -0
- package/dist/slices/youtube/transcript.js +47 -0
- package/dist/updater/plan.js +1 -1
- package/dist/updater/service.js +4 -3
- package/dist/updater/worker.js +6 -4
- package/dist/web/assets/index-B6NbhpjD.css +1 -0
- package/dist/web/assets/index-ComF5gwP.js +142 -0
- package/dist/web/index.html +2 -2
- package/package.json +4 -2
- package/dist/web/assets/index-CZNiHm8P.css +0 -1
- package/dist/web/assets/index-DPgGLX5m.js +0 -137
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
+
import { httpFailure, missingKey, noImage, unreadable } from "../explain.js";
|
|
4
5
|
import { retryAfter } from "../retry-after.js";
|
|
5
6
|
import { downloadImage } from "./bytes.js";
|
|
6
7
|
// The HTTP gateway adapter for fal.ai: `fetch` and the downloader beside this file, no SDK.
|
|
@@ -88,12 +89,12 @@ export function falImage(deps) {
|
|
|
88
89
|
refused(answer, req.prompt);
|
|
89
90
|
const first = answer.images[0];
|
|
90
91
|
if (first === undefined) {
|
|
91
|
-
throw providerError({ kind: "other", message: "fal
|
|
92
|
+
throw providerError({ kind: "other", message: noImage("fal.ai") });
|
|
92
93
|
}
|
|
93
94
|
// The download rides inside the attempt: a link that 404s is a failed attempt.
|
|
94
95
|
return await downloadImage({
|
|
95
96
|
fetch: deps.fetch,
|
|
96
|
-
provider: "fal",
|
|
97
|
+
provider: "fal.ai",
|
|
97
98
|
url: first.url,
|
|
98
99
|
signal: req.signal,
|
|
99
100
|
});
|
|
@@ -104,7 +105,7 @@ function keyOf(deps) {
|
|
|
104
105
|
// `missing_key`, not `auth`, because that rule makes it terminal.
|
|
105
106
|
const key = deps.key();
|
|
106
107
|
if (key === undefined || key === "") {
|
|
107
|
-
throw providerError({ kind: "missing_key", message: "
|
|
108
|
+
throw providerError({ kind: "missing_key", message: missingKey("fal.ai") });
|
|
108
109
|
}
|
|
109
110
|
return key;
|
|
110
111
|
}
|
|
@@ -116,7 +117,7 @@ function refused(answer, prompt) {
|
|
|
116
117
|
if (flags.length > 0 && flags.every((flagged) => flagged)) {
|
|
117
118
|
throw providerError({
|
|
118
119
|
kind: "refusal",
|
|
119
|
-
message: `fal's safety checker
|
|
120
|
+
message: `fal.ai's safety checker blocked the image for this prompt: "${redact(prompt)}". Reword the image prompt in the Images section of Edit project, or choose another image provider in its Providers section.`,
|
|
120
121
|
});
|
|
121
122
|
}
|
|
122
123
|
}
|
|
@@ -140,7 +141,12 @@ async function failure(response) {
|
|
|
140
141
|
const retryAfterMs = retryAfter(response.headers.get("retry-after"));
|
|
141
142
|
return providerError({
|
|
142
143
|
kind: kindOf(response.status),
|
|
143
|
-
message:
|
|
144
|
+
message: httpFailure({
|
|
145
|
+
provider: "fal.ai",
|
|
146
|
+
status: response.status,
|
|
147
|
+
detail: message,
|
|
148
|
+
subject: "image request",
|
|
149
|
+
}),
|
|
144
150
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
145
151
|
});
|
|
146
152
|
}
|
|
@@ -160,7 +166,7 @@ function parse(text) {
|
|
|
160
166
|
if (!parsed.success) {
|
|
161
167
|
throw providerError({
|
|
162
168
|
kind: "other",
|
|
163
|
-
message: "fal
|
|
169
|
+
message: unreadable("fal.ai"),
|
|
164
170
|
});
|
|
165
171
|
}
|
|
166
172
|
return parsed.data;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
+
import { httpFailure, missingKey, noImage, quoted, refusedImage, unreadable } from "../explain.js";
|
|
4
5
|
import { retryAfter } from "../retry-after.js";
|
|
5
6
|
import { describeBytes, sniffImage } from "./bytes.js";
|
|
6
7
|
import { discoverGoogleImages } from "./models.js";
|
|
@@ -92,7 +93,7 @@ function imageOf(answer) {
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
|
-
throw providerError({ kind: "other", message: "Google
|
|
96
|
+
throw providerError({ kind: "other", message: noImage("Google") });
|
|
96
97
|
}
|
|
97
98
|
// The bytes arrive with the call. They are still sniffed rather than trusted: what the port
|
|
98
99
|
// stores is a PNG or a JPEG, and a truncated payload that decodes to something else must
|
|
@@ -104,7 +105,7 @@ function decode(part) {
|
|
|
104
105
|
const claimed = part.mime_type ?? "nothing";
|
|
105
106
|
throw providerError({
|
|
106
107
|
kind: "other",
|
|
107
|
-
message: `Google
|
|
108
|
+
message: `Google sent back something that is not a PNG or JPEG image (labelled ${claimed}, but ${describeBytes(bytes)}). Use Retry stage; if it keeps happening, choose another image model in the Providers section of Edit project.`,
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
return { bytes, mime };
|
|
@@ -113,7 +114,7 @@ function keyOf(deps) {
|
|
|
113
114
|
// `missing_key`, not `auth`, because that rule makes it terminal.
|
|
114
115
|
const key = deps.key();
|
|
115
116
|
if (key === undefined || key === "") {
|
|
116
|
-
throw providerError({ kind: "missing_key", message: "
|
|
117
|
+
throw providerError({ kind: "missing_key", message: missingKey("Google") });
|
|
117
118
|
}
|
|
118
119
|
return key;
|
|
119
120
|
}
|
|
@@ -155,12 +156,21 @@ async function failure(response) {
|
|
|
155
156
|
? parsed.data.error.details?.find((one) => one["@type"] === "type.googleapis.com/google.rpc.RetryInfo")?.retryDelay
|
|
156
157
|
: undefined;
|
|
157
158
|
const retryAfterMs = retryAfter(response.headers.get("retry-after")) ?? googleRetryDelay(delay, message);
|
|
158
|
-
const
|
|
159
|
-
? "Google reports zero image quota for this model. Check the API key's project, billing and model quota in Google AI Studio, or choose another image provider. Retrying will not help until quota is available. "
|
|
160
|
-
: "";
|
|
159
|
+
const detail = message || response.statusText;
|
|
161
160
|
return providerError({
|
|
162
161
|
kind,
|
|
163
|
-
message:
|
|
162
|
+
message: kind === "unsupported"
|
|
163
|
+
? `Google gives this API key no image quota for this model${quoted(`error ${String(response.status)}`, detail)}. Retrying will not help: turn on billing for the key's project in Google AI Studio, or choose another image provider in the Providers section of Edit project.`
|
|
164
|
+
: // PERMISSION_DENIED and FAILED_PRECONDITION also cover keys without access, billing
|
|
165
|
+
// and region limits, so they read as a content refusal only when Google says so.
|
|
166
|
+
kind === "refusal" && refusalWords.test(message)
|
|
167
|
+
? refusedImage("Google", detail)
|
|
168
|
+
: httpFailure({
|
|
169
|
+
provider: "Google",
|
|
170
|
+
status: response.status,
|
|
171
|
+
detail,
|
|
172
|
+
subject: "image request",
|
|
173
|
+
}),
|
|
164
174
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
165
175
|
});
|
|
166
176
|
}
|
|
@@ -177,7 +187,7 @@ function parse(text) {
|
|
|
177
187
|
if (!parsed.success) {
|
|
178
188
|
throw providerError({
|
|
179
189
|
kind: "other",
|
|
180
|
-
message: "Google
|
|
190
|
+
message: unreadable("Google"),
|
|
181
191
|
});
|
|
182
192
|
}
|
|
183
193
|
return parsed.data;
|
|
@@ -64,9 +64,15 @@ export async function discoverGoogleImages(deps) {
|
|
|
64
64
|
function requireKey(deps) {
|
|
65
65
|
const key = deps.key();
|
|
66
66
|
if (key === undefined || key === "")
|
|
67
|
-
throw providerError({
|
|
67
|
+
throw providerError({
|
|
68
|
+
kind: "missing_key",
|
|
69
|
+
message: "Save this provider's API key in Settings → Providers to load its models.",
|
|
70
|
+
});
|
|
68
71
|
return key;
|
|
69
72
|
}
|
|
70
73
|
function unavailable() {
|
|
71
|
-
return providerError({
|
|
74
|
+
return providerError({
|
|
75
|
+
kind: "other",
|
|
76
|
+
message: "Slopify could not load this provider's model list. Check the API key in Settings → Providers and your internet connection, then refresh the list.",
|
|
77
|
+
});
|
|
72
78
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
+
import { httpFailure, missingKey, noImage, refusedImage, unreadable } from "../explain.js";
|
|
4
5
|
import { retryAfter } from "../retry-after.js";
|
|
5
6
|
import { describeBytes, sniffImage } from "./bytes.js";
|
|
6
7
|
import { discoverOpenAiImages } from "./models.js";
|
|
@@ -72,7 +73,7 @@ export function openAiImage(deps) {
|
|
|
72
73
|
}
|
|
73
74
|
const first = parse(await response.text()).data[0]?.b64_json;
|
|
74
75
|
if (first === undefined || first === null || first === "") {
|
|
75
|
-
throw providerError({ kind: "other", message: "OpenAI
|
|
76
|
+
throw providerError({ kind: "other", message: noImage("OpenAI") });
|
|
76
77
|
}
|
|
77
78
|
return decode(first);
|
|
78
79
|
},
|
|
@@ -87,7 +88,7 @@ function decode(b64) {
|
|
|
87
88
|
if (mime === undefined) {
|
|
88
89
|
throw providerError({
|
|
89
90
|
kind: "other",
|
|
90
|
-
message: `OpenAI
|
|
91
|
+
message: `OpenAI sent back something that is not a PNG or JPEG image (${describeBytes(bytes)}). Use Retry stage; if it keeps happening, choose another image model in the Providers section of Edit project.`,
|
|
91
92
|
});
|
|
92
93
|
}
|
|
93
94
|
return { bytes, mime };
|
|
@@ -96,7 +97,7 @@ function keyOf(deps) {
|
|
|
96
97
|
// `missing_key`, not `auth`, because that rule makes it terminal.
|
|
97
98
|
const key = deps.key();
|
|
98
99
|
if (key === undefined || key === "") {
|
|
99
|
-
throw providerError({ kind: "missing_key", message: "
|
|
100
|
+
throw providerError({ kind: "missing_key", message: missingKey("OpenAI") });
|
|
100
101
|
}
|
|
101
102
|
return key;
|
|
102
103
|
}
|
|
@@ -122,9 +123,17 @@ async function failure(response) {
|
|
|
122
123
|
// The provider's own words, through the redactor - an error body may quote the key back.
|
|
123
124
|
const message = redact(parsed.success ? parsed.data.error.message : text.trim() || response.statusText);
|
|
124
125
|
const retryAfterMs = retryAfter(response.headers.get("retry-after"));
|
|
126
|
+
const kind = kindOf(response.status, parsed.success ? parsed.data.error.code : undefined);
|
|
125
127
|
return providerError({
|
|
126
|
-
kind
|
|
127
|
-
message:
|
|
128
|
+
kind,
|
|
129
|
+
message: kind === "refusal"
|
|
130
|
+
? refusedImage("OpenAI", message)
|
|
131
|
+
: httpFailure({
|
|
132
|
+
provider: "OpenAI",
|
|
133
|
+
status: response.status,
|
|
134
|
+
detail: message,
|
|
135
|
+
subject: "image request",
|
|
136
|
+
}),
|
|
128
137
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
129
138
|
});
|
|
130
139
|
}
|
|
@@ -133,7 +142,7 @@ function parse(text) {
|
|
|
133
142
|
if (!parsed.success) {
|
|
134
143
|
throw providerError({
|
|
135
144
|
kind: "other",
|
|
136
|
-
message: "OpenAI
|
|
145
|
+
message: unreadable("OpenAI"),
|
|
137
146
|
});
|
|
138
147
|
}
|
|
139
148
|
return parsed.data;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
+
import { httpFailure, missingKey, noImage, providerSaid, refusedImage, unreadable, } from "../explain.js";
|
|
4
5
|
import { retryAfter } from "../retry-after.js";
|
|
5
6
|
import { downloadImage } from "./bytes.js";
|
|
6
7
|
// The HTTP gateway adapter for Replicate: `fetch`, the injected clock and the downloader
|
|
@@ -81,7 +82,7 @@ async function settle(deps, first, signal) {
|
|
|
81
82
|
if (next === undefined) {
|
|
82
83
|
throw providerError({
|
|
83
84
|
kind: "other",
|
|
84
|
-
message:
|
|
85
|
+
message: unreadable("Replicate"),
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
88
|
await deps.clock.sleep(replicatePollMs, signal);
|
|
@@ -100,7 +101,7 @@ function outputOf(current) {
|
|
|
100
101
|
const output = current.output;
|
|
101
102
|
const url = typeof output === "string" ? output : output?.[0];
|
|
102
103
|
if (url === undefined || url === "") {
|
|
103
|
-
throw providerError({ kind: "other", message: "Replicate
|
|
104
|
+
throw providerError({ kind: "other", message: noImage("Replicate") });
|
|
104
105
|
}
|
|
105
106
|
return url;
|
|
106
107
|
}
|
|
@@ -114,16 +115,19 @@ function declined(current) {
|
|
|
114
115
|
// ceiling: read off the sentence, because a failed prediction carries no machine-readable
|
|
115
116
|
// reason. A phrase the list does not know costs the user three more images; the upgrade
|
|
116
117
|
// is a structured field, when Replicate ships one.
|
|
118
|
+
const refusal = refusalWords.test(message);
|
|
117
119
|
return providerError({
|
|
118
|
-
kind:
|
|
119
|
-
message:
|
|
120
|
+
kind: refusal ? "refusal" : "other",
|
|
121
|
+
message: refusal
|
|
122
|
+
? refusedImage("Replicate", message)
|
|
123
|
+
: providerSaid("Replicate", "could not make the image", message, "Use Retry stage; if it keeps happening, reword the image prompt in the Images section of Edit project or choose another image model in its Providers section."),
|
|
120
124
|
});
|
|
121
125
|
}
|
|
122
126
|
function auth(deps) {
|
|
123
127
|
// `missing_key`, not `auth`, because that rule makes it terminal.
|
|
124
128
|
const key = deps.key();
|
|
125
129
|
if (key === undefined || key === "") {
|
|
126
|
-
throw providerError({ kind: "missing_key", message: "
|
|
130
|
+
throw providerError({ kind: "missing_key", message: missingKey("Replicate") });
|
|
127
131
|
}
|
|
128
132
|
return { Authorization: `Bearer ${key}` };
|
|
129
133
|
}
|
|
@@ -149,7 +153,12 @@ async function failure(response) {
|
|
|
149
153
|
const retryAfterMs = retryAfter(response.headers.get("retry-after"));
|
|
150
154
|
return providerError({
|
|
151
155
|
kind: kindOf(response.status),
|
|
152
|
-
message:
|
|
156
|
+
message: httpFailure({
|
|
157
|
+
provider: "Replicate",
|
|
158
|
+
status: response.status,
|
|
159
|
+
detail: message || response.statusText,
|
|
160
|
+
subject: "image request",
|
|
161
|
+
}),
|
|
153
162
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
154
163
|
});
|
|
155
164
|
}
|
|
@@ -158,7 +167,7 @@ function parse(text) {
|
|
|
158
167
|
if (!parsed.success) {
|
|
159
168
|
throw providerError({
|
|
160
169
|
kind: "other",
|
|
161
|
-
message: "Replicate
|
|
170
|
+
message: unreadable("Replicate"),
|
|
162
171
|
});
|
|
163
172
|
}
|
|
164
173
|
return parsed.data;
|
|
@@ -38,7 +38,7 @@ const responseSchema = z.object({
|
|
|
38
38
|
});
|
|
39
39
|
const supportedEfforts = ["low", "medium", "high", "xhigh"];
|
|
40
40
|
function unavailable() {
|
|
41
|
-
return new Error("Claude Code
|
|
41
|
+
return new Error("Slopify could not get the model list from the Claude Code CLI. Check the CLI is installed and signed in (Settings → Providers), then refresh the list, or type an exact model ID.");
|
|
42
42
|
}
|
|
43
43
|
function discoveryEnv() {
|
|
44
44
|
const names = [
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
+
import { cliCheck, cliReported, commandOf } from "../explain.js";
|
|
4
5
|
import { nodeClaudeCodeModels } from "./claude-code-models.js";
|
|
5
6
|
import { cliLoginError } from "./cli-login-error.js";
|
|
6
7
|
import { documentToolName, documentWorkspace, } from "./document-workspace.js";
|
|
7
|
-
import { cliEvent, cliInput, cliShaped, endedWithout, promptOf, stopCliRun } from "./run-cli.js";
|
|
8
|
+
import { cliEvent, cliInput, cliShaped, endedWithout, promptOf, stopCliRun, stuckCli, } from "./run-cli.js";
|
|
8
9
|
import { lines } from "./sse-lines.js";
|
|
9
10
|
// The local-agent adapter for Claude Code: spawned non-interactively, authenticated by the
|
|
10
11
|
// CLI's own login, no key anywhere in this file. Readiness is not computed here - `adapters/**`
|
|
@@ -142,7 +143,7 @@ export function claudeCodeLlm(deps) {
|
|
|
142
143
|
catch {
|
|
143
144
|
throw providerError({
|
|
144
145
|
kind: "unavailable",
|
|
145
|
-
message: "
|
|
146
|
+
message: "Slopify could not hand the whole prompt to the Claude Code CLI, so it cannot tell what the CLI worked on (it may already have used your quota). Use Retry stage to run it again.",
|
|
146
147
|
});
|
|
147
148
|
}
|
|
148
149
|
if (result.is_error === true || result.subtype !== "success") {
|
|
@@ -151,7 +152,7 @@ export function claudeCodeLlm(deps) {
|
|
|
151
152
|
throw login;
|
|
152
153
|
throw providerError({
|
|
153
154
|
kind: kindOf(result.api_error_status ?? null),
|
|
154
|
-
message: redact(result.result ?? result.subtype),
|
|
155
|
+
message: cliReported(binary, redact(result.result ?? result.subtype), nextStep(binary, result.api_error_status ?? null)),
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
158
|
documents?.verifyRead();
|
|
@@ -188,15 +189,13 @@ export function claudeCodeLlm(deps) {
|
|
|
188
189
|
req.signal.throwIfAborted();
|
|
189
190
|
// The stream ended with no result event at all.
|
|
190
191
|
if (!run)
|
|
191
|
-
throw new Error("Claude Code could not be started.");
|
|
192
|
+
throw new Error("The Claude Code CLI could not be started. Check it is installed and set up in Settings → Providers, then use Retry stage.");
|
|
192
193
|
const login = cliLoginError("claude-code", run.stderr());
|
|
193
194
|
if (login)
|
|
194
195
|
throw login;
|
|
195
196
|
throw providerError({
|
|
196
197
|
kind: "other",
|
|
197
|
-
message: ended === undefined
|
|
198
|
-
? `the ${binary} CLI did not stop after forced termination`
|
|
199
|
-
: endedWithout(binary, ended, run.stderr()),
|
|
198
|
+
message: ended === undefined ? stuckCli(binary) : endedWithout(binary, ended, run.stderr()),
|
|
200
199
|
});
|
|
201
200
|
}
|
|
202
201
|
return {
|
|
@@ -218,6 +217,16 @@ function kindOf(status) {
|
|
|
218
217
|
}
|
|
219
218
|
return "other";
|
|
220
219
|
}
|
|
220
|
+
// What most often fixes the upstream status the CLI passed on.
|
|
221
|
+
function nextStep(binary, status) {
|
|
222
|
+
if (status === 401 || status === 403)
|
|
223
|
+
return `Run "${commandOf(binary)} auth login" in a terminal to sign in again, then use Retry stage.`;
|
|
224
|
+
if (status === 429)
|
|
225
|
+
return "Your Claude usage limit may be used up: wait until it resets, then use Retry stage.";
|
|
226
|
+
if (status === 404)
|
|
227
|
+
return "The chosen model may not exist or not be available to your account: choose another in the Providers section of Edit project, then use Retry stage.";
|
|
228
|
+
return cliCheck(binary);
|
|
229
|
+
}
|
|
221
230
|
// `modelUsage` is keyed by model id and a run may touch more than one - a fallback model,
|
|
222
231
|
// a sub-agent - so the counts are summed. An empty object means the CLI reported none.
|
|
223
232
|
function usageOf(usage) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { providerError } from "../../kernel/ports/model.js";
|
|
2
|
+
import { cliName } from "../explain.js";
|
|
2
3
|
export function cliLoginError(provider, text) {
|
|
3
4
|
if (/invalid api key/i.test(text))
|
|
4
5
|
return undefined;
|
|
@@ -11,6 +12,6 @@ export function cliLoginError(provider, text) {
|
|
|
11
12
|
: "codex login";
|
|
12
13
|
return providerError({
|
|
13
14
|
kind: "missing_key",
|
|
14
|
-
message:
|
|
15
|
+
message: `The ${cliName(provider)} is not signed in, or its sign-in has expired. Open a terminal on the computer running the CLI, run "${command}" and sign in, then use Retry stage.`,
|
|
15
16
|
});
|
|
16
17
|
}
|
|
@@ -58,7 +58,7 @@ export async function nodeCodexModels(env = process.env, binary = "codex", timeo
|
|
|
58
58
|
}
|
|
59
59
|
catch {
|
|
60
60
|
// Do not expose cache contents, home paths or raw JSON errors to the browser.
|
|
61
|
-
throw new Error("
|
|
61
|
+
throw new Error("Slopify could not get the model list from the Codex CLI. Check the CLI is installed and signed in (Settings → Providers), then refresh the list, or type an exact model ID.");
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -4,10 +4,11 @@ import { join } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { redact } from "../../kernel/log.js";
|
|
6
6
|
import { providerError } from "../../kernel/ports/model.js";
|
|
7
|
+
import { cliCheck, cliReported } from "../explain.js";
|
|
7
8
|
import { cliLoginError } from "./cli-login-error.js";
|
|
8
9
|
import { nodeCodexModels } from "./codex-models.js";
|
|
9
10
|
import { documentServerName, documentTool, documentWorkspace, } from "./document-workspace.js";
|
|
10
|
-
import { cliEvent, cliInput, cliShaped, deliveredInput, endedWithout, promptOf, stopCliRun, } from "./run-cli.js";
|
|
11
|
+
import { cliEvent, cliInput, cliShaped, deliveredInput, endedWithout, promptOf, stopCliRun, stuckCli, } from "./run-cli.js";
|
|
11
12
|
import { lines } from "./sse-lines.js";
|
|
12
13
|
// The local-agent adapter for the Codex CLI. Same shape as Claude Code's and a different
|
|
13
14
|
// vocabulary: Codex writes a JSONL thread of `thread.started`, `item.*` and `turn.*`
|
|
@@ -164,7 +165,7 @@ export function codexLlm(deps) {
|
|
|
164
165
|
throw login;
|
|
165
166
|
throw providerError({
|
|
166
167
|
kind: "other",
|
|
167
|
-
message: redact(cliShaped(binary, turnFailed, event.value).error.message),
|
|
168
|
+
message: cliReported(binary, redact(cliShaped(binary, turnFailed, event.value).error.message), cliCheck(binary)),
|
|
168
169
|
});
|
|
169
170
|
}
|
|
170
171
|
if (event.type === "error") {
|
|
@@ -173,7 +174,7 @@ export function codexLlm(deps) {
|
|
|
173
174
|
throw login;
|
|
174
175
|
throw providerError({
|
|
175
176
|
kind: "other",
|
|
176
|
-
message: redact(cliShaped(binary, errorEvent, event.value).message),
|
|
177
|
+
message: cliReported(binary, redact(cliShaped(binary, errorEvent, event.value).message), cliCheck(binary)),
|
|
177
178
|
});
|
|
178
179
|
}
|
|
179
180
|
}
|
|
@@ -202,9 +203,7 @@ export function codexLlm(deps) {
|
|
|
202
203
|
throw login;
|
|
203
204
|
throw providerError({
|
|
204
205
|
kind: "other",
|
|
205
|
-
message: ended === undefined
|
|
206
|
-
? `the ${binary} CLI did not stop after forced termination`
|
|
207
|
-
: endedWithout(binary, ended, run.stderr()),
|
|
206
|
+
message: ended === undefined ? stuckCli(binary) : endedWithout(binary, ended, run.stderr()),
|
|
208
207
|
});
|
|
209
208
|
}
|
|
210
209
|
return {
|
|
@@ -16,7 +16,7 @@ export function documentWorkspace(input) {
|
|
|
16
16
|
if (!parsed.success)
|
|
17
17
|
throw providerError({
|
|
18
18
|
kind: "unsupported",
|
|
19
|
-
message: "
|
|
19
|
+
message: "The research notes are too large, or not in a form the AI model can be given, so nothing was sent. Make the inputs shorter in Edit project, then use Retry stage; if it keeps happening, use Download diagnostics in Settings and report it.",
|
|
20
20
|
});
|
|
21
21
|
const documents = parsed.data;
|
|
22
22
|
const directory = mkdtempSync(join(tmpdir(), "slopify-documents-"));
|
|
@@ -82,6 +82,6 @@ export function documentWorkspace(input) {
|
|
|
82
82
|
function unread() {
|
|
83
83
|
return providerError({
|
|
84
84
|
kind: "unavailable",
|
|
85
|
-
message: "The
|
|
85
|
+
message: "The AI model did not read all of the research notes, so its answer was not used (the attempt may still have used your quota). Use Retry stage; if it keeps happening, choose another model in the Providers section of Edit project.",
|
|
86
86
|
});
|
|
87
87
|
}
|
|
@@ -38,7 +38,7 @@ export async function nodeGeminiModels(binary) {
|
|
|
38
38
|
throw new Error("No installed model metadata");
|
|
39
39
|
}
|
|
40
40
|
catch {
|
|
41
|
-
throw new Error("Gemini CLI
|
|
41
|
+
throw new Error("Slopify could not get the model list from the Gemini CLI. Check the CLI is installed (Settings → Providers), then refresh the list, or type a model name the Gemini CLI accepts.");
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
async function bundledModels(entry) {
|
|
@@ -33,7 +33,7 @@ export function geminiWorkspace(webSearch, request, documents, env = process.env
|
|
|
33
33
|
catch {
|
|
34
34
|
throw providerError({
|
|
35
35
|
kind: "unavailable",
|
|
36
|
-
message: "Gemini CLI
|
|
36
|
+
message: "Slopify could not read the Gemini CLI's settings file (settings.json in the .gemini folder of your home directory), which says how it signs in. Fix or remove that file, run gemini in a terminal to sign in again, then use Retry stage.",
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
const oauth = join(originalState, "oauth_creds.json");
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
+
import { cliCheck, cliName, cliReported } from "../explain.js";
|
|
4
5
|
import { documentWorkspace } from "./document-workspace.js";
|
|
5
6
|
import { nodeGeminiModels } from "./gemini-models.js";
|
|
6
7
|
import { geminiWorkspace } from "./gemini-workspace.js";
|
|
7
|
-
import { cliEvent, cliInput, cliShaped, deliveredInput, endedWithout, promptOf, stopCliRun, } from "./run-cli.js";
|
|
8
|
+
import { cliEvent, cliInput, cliShaped, deliveredInput, endedWithout, promptOf, stopCliRun, stuckCli, } from "./run-cli.js";
|
|
8
9
|
import { lines } from "./sse-lines.js";
|
|
9
10
|
export const geminiBinary = "gemini";
|
|
10
11
|
// Verified against installed Gemini CLI 0.16.0's config/nonInteractiveCli sources.
|
|
@@ -65,12 +66,12 @@ export function geminiLlm(deps) {
|
|
|
65
66
|
else if (event.type === "error") {
|
|
66
67
|
const error = cliShaped(binary, errorSchema, event.value);
|
|
67
68
|
if (error.severity !== "warning")
|
|
68
|
-
throw failure(error.message);
|
|
69
|
+
throw failure(binary, error.message);
|
|
69
70
|
}
|
|
70
71
|
else if (event.type === "result") {
|
|
71
72
|
const result = cliShaped(binary, resultSchema, event.value);
|
|
72
73
|
if (result.status !== "success")
|
|
73
|
-
throw failure(result.error?.message ?? result.status);
|
|
74
|
+
throw failure(binary, result.error?.message ?? result.status);
|
|
74
75
|
await deliveredInput(run);
|
|
75
76
|
documents?.verifyRead();
|
|
76
77
|
yield {
|
|
@@ -103,12 +104,10 @@ export function geminiLlm(deps) {
|
|
|
103
104
|
}
|
|
104
105
|
req.signal.throwIfAborted();
|
|
105
106
|
if (run === undefined)
|
|
106
|
-
throw failure(`
|
|
107
|
+
throw failure(binary, "", `The ${cliName(binary)} could not be started. Check it is installed and set up in Settings → Providers, then use Retry stage.`);
|
|
107
108
|
if (authRequired(run.stderr()))
|
|
108
109
|
throw authFailure();
|
|
109
|
-
throw failure(ended === undefined
|
|
110
|
-
? `the ${binary} CLI did not stop after forced termination`
|
|
111
|
-
: endedWithout(binary, ended, run.stderr()));
|
|
110
|
+
throw failure(binary, run.stderr(), ended === undefined ? stuckCli(binary) : endedWithout(binary, ended, run.stderr()));
|
|
112
111
|
}
|
|
113
112
|
return {
|
|
114
113
|
id: "gemini",
|
|
@@ -117,16 +116,21 @@ export function geminiLlm(deps) {
|
|
|
117
116
|
complete,
|
|
118
117
|
};
|
|
119
118
|
}
|
|
120
|
-
|
|
119
|
+
// `message` is what the CLI said, read for the two failures that need the user's hand;
|
|
120
|
+
// `sentence` replaces the default wording when Slopify already knows what happened.
|
|
121
|
+
function failure(binary, message, sentence) {
|
|
121
122
|
if (/(?:#?3501\b|do not have a valid licen[cs]e of this product)/i.test(message)) {
|
|
122
123
|
return providerError({
|
|
123
124
|
kind: "unsupported",
|
|
124
|
-
message: "Gemini CLI
|
|
125
|
+
message: "Google refused the Gemini CLI because the signed-in account has no licence for it (#3501). Update the Gemini CLI, run gemini in a terminal to sign in again, then use Retry stage; for a work or school account, ask your administrator for a Gemini licence.",
|
|
125
126
|
});
|
|
126
127
|
}
|
|
127
128
|
return authRequired(message)
|
|
128
129
|
? authFailure()
|
|
129
|
-
: providerError({
|
|
130
|
+
: providerError({
|
|
131
|
+
kind: "other",
|
|
132
|
+
message: sentence ?? cliReported(binary, redact(message), cliCheck(binary)),
|
|
133
|
+
});
|
|
130
134
|
}
|
|
131
135
|
function authRequired(message) {
|
|
132
136
|
return /Code Assist login required|authorize the application|authorization code|Please set an Auth method|re-authenticate|reauthenticate/i.test(message);
|
|
@@ -134,7 +138,7 @@ function authRequired(message) {
|
|
|
134
138
|
function authFailure() {
|
|
135
139
|
return providerError({
|
|
136
140
|
kind: "missing_key",
|
|
137
|
-
message:
|
|
141
|
+
message: 'The Gemini CLI is not signed in, or its sign-in has expired. Open a terminal on the computer running the CLI, run "gemini" and sign in, then use Retry stage.',
|
|
138
142
|
});
|
|
139
143
|
}
|
|
140
144
|
async function* authChecked(source) {
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { documentMessages } from "../../kernel/ports/llm-documents.js";
|
|
4
4
|
import { providerError } from "../../kernel/ports/model.js";
|
|
5
|
+
import { droppedStream, httpFailure, missingKey, providerSaid, refreshList, unreadable, } from "../explain.js";
|
|
5
6
|
import { retryAfter } from "../retry-after.js";
|
|
6
7
|
import { sseData } from "./sse-lines.js";
|
|
7
8
|
// The HTTP gateway adapter: the platform's own `fetch` plus the line reader beside this
|
|
@@ -62,7 +63,7 @@ export function openRouterLlm(deps) {
|
|
|
62
63
|
throw await failure(response);
|
|
63
64
|
}
|
|
64
65
|
if (response.body === null) {
|
|
65
|
-
throw providerError({ kind: "other", message: "OpenRouter
|
|
66
|
+
throw providerError({ kind: "other", message: droppedStream("OpenRouter") });
|
|
66
67
|
}
|
|
67
68
|
let usage = null;
|
|
68
69
|
let finishReason = null;
|
|
@@ -81,7 +82,7 @@ export function openRouterLlm(deps) {
|
|
|
81
82
|
// frame; without this the stage would store half an article as a success.
|
|
82
83
|
throw providerError({
|
|
83
84
|
kind: kindOf(statusOf(chunk.error.code)),
|
|
84
|
-
message: redact(chunk.error.message),
|
|
85
|
+
message: streamFailure(statusOf(chunk.error.code), redact(chunk.error.message)),
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
88
|
const choice = chunk.choices?.[0];
|
|
@@ -108,7 +109,7 @@ export function openRouterLlm(deps) {
|
|
|
108
109
|
// Saying so is what stops a truncated answer being stored as a whole one.
|
|
109
110
|
throw providerError({
|
|
110
111
|
kind: "other",
|
|
111
|
-
message: "OpenRouter
|
|
112
|
+
message: droppedStream("OpenRouter"),
|
|
112
113
|
});
|
|
113
114
|
}
|
|
114
115
|
yield { type: "done", usage, finishReason };
|
|
@@ -122,13 +123,13 @@ export function openRouterLlm(deps) {
|
|
|
122
123
|
headers: headers(deps.key()),
|
|
123
124
|
});
|
|
124
125
|
if (!response.ok) {
|
|
125
|
-
throw await failure(response);
|
|
126
|
+
throw await failure(response, refreshList);
|
|
126
127
|
}
|
|
127
128
|
const parsed = modelList.safeParse(await response.json());
|
|
128
129
|
if (!parsed.success) {
|
|
129
130
|
throw providerError({
|
|
130
131
|
kind: "other",
|
|
131
|
-
message: "OpenRouter
|
|
132
|
+
message: unreadable("OpenRouter"),
|
|
132
133
|
});
|
|
133
134
|
}
|
|
134
135
|
// /models defaults to text output. Also reject an explicit non-text row
|
|
@@ -146,7 +147,7 @@ function headers(key) {
|
|
|
146
147
|
// rather than `auth` because the same rule makes it terminal: there is nothing to
|
|
147
148
|
// retry until the user saves a key, and the wrapper is where that is decided.
|
|
148
149
|
if (key === undefined || key === "") {
|
|
149
|
-
throw providerError({ kind: "missing_key", message: "
|
|
150
|
+
throw providerError({ kind: "missing_key", message: missingKey("OpenRouter") });
|
|
150
151
|
}
|
|
151
152
|
return { Authorization: `Bearer ${key}`, ...appHeaders };
|
|
152
153
|
}
|
|
@@ -163,7 +164,7 @@ function kindOf(status) {
|
|
|
163
164
|
// error contract first, which is not this adapter's to widen.
|
|
164
165
|
return "other";
|
|
165
166
|
}
|
|
166
|
-
async function failure(response) {
|
|
167
|
+
async function failure(response, next) {
|
|
167
168
|
const text = await response.text().catch(() => "");
|
|
168
169
|
const parsed = errorBody.safeParse(safeJson(text));
|
|
169
170
|
// The provider's own words, through the redactor - an error body may quote the key back.
|
|
@@ -171,10 +172,21 @@ async function failure(response) {
|
|
|
171
172
|
const retryAfterMs = retryAfter(response.headers.get("retry-after"));
|
|
172
173
|
return providerError({
|
|
173
174
|
kind: kindOf(response.status),
|
|
174
|
-
message:
|
|
175
|
+
message: httpFailure({
|
|
176
|
+
provider: "OpenRouter",
|
|
177
|
+
status: response.status,
|
|
178
|
+
detail: message,
|
|
179
|
+
next,
|
|
180
|
+
}),
|
|
175
181
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
176
182
|
});
|
|
177
183
|
}
|
|
184
|
+
// A mid-stream error carries an HTTP-like code; with none, the provider's words are all there is.
|
|
185
|
+
function streamFailure(status, detail) {
|
|
186
|
+
return status >= 400
|
|
187
|
+
? httpFailure({ provider: "OpenRouter", status, detail })
|
|
188
|
+
: providerSaid("OpenRouter", "stopped part-way through the answer", detail, "Use Retry stage; if it keeps happening, choose another model in the Providers section of Edit project.");
|
|
189
|
+
}
|
|
178
190
|
function statusOf(code) {
|
|
179
191
|
return typeof code === "number" ? code : Number(code ?? 0);
|
|
180
192
|
}
|
|
@@ -186,7 +198,7 @@ function parseChunk(data) {
|
|
|
186
198
|
// a half-written frame is noise, and the wrapper stores whatever is thrown.
|
|
187
199
|
throw providerError({
|
|
188
200
|
kind: "other",
|
|
189
|
-
message: "OpenRouter
|
|
201
|
+
message: unreadable("OpenRouter"),
|
|
190
202
|
});
|
|
191
203
|
}
|
|
192
204
|
return parsed.data;
|