added Gab AI affiliate link; changed Ollama abort procedure
This commit is contained in:
parent
96663f7c88
commit
d990d0dd71
@ -16,27 +16,33 @@ or is aborted, the abort signal mechanism is cleaned up end-to-end.
|
||||
|
||||
```
|
||||
[Frontend] --abortWorkOrder(cb)--> [CodeSession] --abortWorkOrder(cb)--> [Drone]
|
||||
|
|
||||
AgentService
|
||||
.abortCurrentWorkOrder()
|
||||
|
|
||||
abortController.abort()
|
||||
|
|
||||
cb(true, "Abort signaled")
|
||||
|
|
||||
|
|
||||
AgentService
|
||||
.abortCurrentWorkOrder()
|
||||
|
|
||||
abortController.abort()
|
||||
|
|
||||
AiService.abort()
|
||||
(calls AI provider's
|
||||
abort() which for
|
||||
Ollama terminates the
|
||||
HTTP request immediately)
|
||||
|
|
||||
cb(true, "Abort signaled")
|
||||
|
|
||||
[Frontend] <------- callback received, show "Aborting..." -----------------------+
|
||||
|
|
||||
(AbortError thrown
|
||||
in AI provider)
|
||||
|
|
||||
AgentService catches
|
||||
AbortError, emits
|
||||
workOrderComplete(turnId, true, "aborted")
|
||||
|
|
||||
|
|
||||
(AbortError thrown
|
||||
in AI provider)
|
||||
|
|
||||
AgentService catches
|
||||
AbortError, emits
|
||||
workOrderComplete(turnId, true, "aborted")
|
||||
|
|
||||
[DroneSession] --workOrderComplete(turnId, true, "aborted")--> [CodeSession]
|
||||
| |
|
||||
turn.status = turn.status = "aborted"
|
||||
ChatTurnStatus.Aborted displayed subtly
|
||||
| |
|
||||
turn.status = turn.status = "aborted"
|
||||
ChatTurnStatus.Aborted displayed subtly
|
||||
```
|
||||
|
||||
**Key design decision**: `workOrderComplete` is used with `success=true` and
|
||||
@ -74,6 +80,10 @@ No `SocketEvents` changes needed — the drone listens on its own socket.
|
||||
- `IAiChatOptions`
|
||||
- `IAiGenerateOptions`
|
||||
|
||||
Also added a default (no-op) `abort()` method on the `AiApi` abstract class.
|
||||
Provider-specific implementations override this to forcefully terminate
|
||||
in-progress HTTP requests.
|
||||
|
||||
**`packages/ai/src/ollama.ts`** — In both `generate()` and `chat()`:
|
||||
|
||||
1. Pre-request check: `if (options.signal?.aborted) throw ...`
|
||||
@ -81,6 +91,9 @@ No `SocketEvents` changes needed — the drone listens on its own socket.
|
||||
3. Per-chunk check at top of `for await` loop:
|
||||
`if (options.signal?.aborted) throw new DOMException("The operation was aborted", "AbortError")`
|
||||
|
||||
Additionally, `OllamaAiApi` overrides `abort()` to call `this.client.abort()`,
|
||||
which immediately terminates the underlying HTTP request to the Ollama server.
|
||||
|
||||
**`packages/ai/src/openai.ts`** — Same pattern in:
|
||||
|
||||
- `generate()` — pre-check, pass `signal` to SDK, per-chunk in `for await`
|
||||
@ -108,7 +121,18 @@ and is detected downstream via `error.name === "AbortError"`.
|
||||
```
|
||||
- `finally` block sets `this.abortController = null`
|
||||
- Public method `abortCurrentWorkOrder(): boolean` — calls
|
||||
`this.abortController?.abort()`, returns `true` if there was a controller
|
||||
`this.abortController?.abort()` then `AiService.abort()`, returns `true` if
|
||||
there was a controller. The `AiService.abort()` call provides provider-specific
|
||||
forceful abort (e.g., `OllamaAiApi.abort()` → `this.client.abort()`).
|
||||
|
||||
**`gadget-drone/src/services/ai.ts`**:
|
||||
|
||||
- Property: `private activeApi: AiApi | null = null` — tracks the most recently
|
||||
created `AiApi` instance
|
||||
- `getApi()` stores each created instance on `this.activeApi`
|
||||
- `chat()` and `generate()` clear `this.activeApi = null` in `finally` blocks
|
||||
- Public method `abort(): void` — calls `this.activeApi?.abort()`, then clears
|
||||
the reference
|
||||
|
||||
**`gadget-drone/src/gadget-drone.ts`**:
|
||||
|
||||
@ -193,10 +217,11 @@ status: "processing" | "finished" | "aborted" | "error";
|
||||
| `.gitignore` | Add root-level log file patterns |
|
||||
| `packages/api/src/messages/ide.ts` | New `AbortWorkOrderCallback`, `AbortWorkOrderMessage` |
|
||||
| `packages/api/src/messages/socket.ts` | Register `abortWorkOrder` in both event interfaces |
|
||||
| `packages/ai/src/api.ts` | `signal?: AbortSignal` on `IAiChatOptions`, `IAiGenerateOptions` |
|
||||
| `packages/ai/src/ollama.ts` | Pre-request and per-chunk abort checks, signal pass-through |
|
||||
| `packages/ai/src/api.ts` | `signal?: AbortSignal` on `IAiChatOptions`, `IAiGenerateOptions`; `abort()` on `AiApi` |
|
||||
| `packages/ai/src/ollama.ts` | Pre-request and per-chunk abort checks, signal pass-through; `abort()` override |
|
||||
| `packages/ai/src/openai.ts` | Same for `generate()`, `readStreamingChatCompletion()`, `readNonStreamingChatCompletion()` |
|
||||
| `gadget-drone/src/services/agent.ts` | `AbortController` property, abort detection, `abortCurrentWorkOrder()` |
|
||||
| `gadget-drone/src/services/agent.ts` | `AbortController` property, abort detection, `abortCurrentWorkOrder()` calls `AiService.abort()` |
|
||||
| `gadget-drone/src/services/ai.ts` | Track active `AiApi` instance, `abort()` method, cleanup in `finally` blocks |
|
||||
| `gadget-drone/src/gadget-drone.ts` | `onAbortWorkOrder` socket handler |
|
||||
| `gadget-code/src/lib/code-session.ts` | Forward `abortWorkOrder` to drone |
|
||||
| `gadget-code/src/lib/drone-session.ts` | Detect `"aborted"` in `workOrderComplete` |
|
||||
@ -214,9 +239,16 @@ status: "processing" | "finished" | "aborted" | "error";
|
||||
event or change the `workOrderComplete` signature.
|
||||
- The `AbortController` is created per-process() call and cleaned up in
|
||||
`finally`. It is never shared across work orders.
|
||||
- The abort callback returns immediately after the controller is signaled. The
|
||||
AbortError propagates asynchronously through the SDK stream and is caught
|
||||
separately in the agent loop.
|
||||
- The abort callback returns immediately after the controller is signaled and
|
||||
`AiService.abort()` is called. The AbortError propagates asynchronously
|
||||
through the SDK stream and is caught separately in the agent loop.
|
||||
- `AiService.abort()` provides provider-specific forceful abort (e.g., Ollama's
|
||||
`client.abort()` terminates the HTTP request immediately). This is called
|
||||
_in addition to_ the `AbortController.abort()` signal so that both the
|
||||
AbortSignal chain and the provider-specific abort mechanism are triggered.
|
||||
- The active `AiApi` instance is tracked on `AiService.activeApi` and is set to
|
||||
`null` in the `finally` block of each `chat()`/`generate()` call, so the
|
||||
reference is never stale.
|
||||
- Subagent loops also receive the abort signal (via `this.abortController?.signal`)
|
||||
and abort naturally — no special subagent abort handling needed.
|
||||
- The Esc handler is a global `window` keydown listener. It is registered ONLY
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
providerApi,
|
||||
} from "../lib/api";
|
||||
import { socketClient } from "../lib/socket";
|
||||
import gabAiImage from "./assets/gab-ai.jpeg";
|
||||
|
||||
interface ProjectManagerProps {
|
||||
user: User | null;
|
||||
@ -231,7 +232,11 @@ interface ProjectInspectorProps {
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
function ProjectInspector({ project, onDelete, onUpdate }: ProjectInspectorProps) {
|
||||
function ProjectInspector({
|
||||
project,
|
||||
onDelete,
|
||||
onUpdate,
|
||||
}: ProjectInspectorProps) {
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
@ -279,7 +284,9 @@ function ProjectInspector({ project, onDelete, onUpdate }: ProjectInspectorProps
|
||||
</div>
|
||||
<div className="p-4 bg-bg-secondary border border-border-default rounded">
|
||||
<div className="text-sm text-text-muted mb-1">Slug</div>
|
||||
<div className="font-mono text-text-primary">{project.slug}</div>
|
||||
<div className="font-mono text-text-primary">
|
||||
{project.slug}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -652,118 +659,152 @@ function NewChatSessionModal({
|
||||
}
|
||||
};
|
||||
|
||||
const affiliateHref = "https://gab.ai/?ref=c7182f6e";
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50">
|
||||
<div className="bg-bg-secondary border border-border-default rounded p-6 w-full max-w-md">
|
||||
<h3 className="text-lg font-semibold mb-4">New Chat Session</h3>
|
||||
{selectedDrone && (
|
||||
<div className="mb-4 p-3 bg-bg-tertiary border border-border-default rounded">
|
||||
<div className="text-xs text-text-muted mb-1">Selected Drone</div>
|
||||
<div className="font-mono text-sm text-text-primary">
|
||||
{selectedDrone.hostname}
|
||||
<div className="bg-bg-secondary border border-border-default rounded p-6 w-full max-w-2xl flex gap-6">
|
||||
<div className="w-48 flex-shrink-0 flex flex-col">
|
||||
<a
|
||||
href={affiliateHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<img src={gabAiImage} alt="Gab AI" className="w-full rounded" />
|
||||
</a>
|
||||
<p className="flex-1 flex items-center justify-center text-center text-lg text-text-secondary px-1">
|
||||
Need big-brain inference for this session?
|
||||
</p>
|
||||
<p className="flex-1 flex items-center justify-center text-left text-sm text-text-secondary px-1">
|
||||
Sign up for Gab AI and get 250 credits to see if it's right for you!
|
||||
</p>
|
||||
<a
|
||||
href={affiliateHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<button className="w-full px-4 py-2 border border-[#00d178] text-[#00d178] rounded hover:bg-[#00d178] hover:text-white text-sm font-medium transition-colors">
|
||||
Get Started
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex-1 border-l border-border-subtle pl-6">
|
||||
<h3 className="text-lg font-semibold mb-4">New Chat Session</h3>
|
||||
{selectedDrone && (
|
||||
<div className="mb-4 p-3 bg-bg-tertiary border border-border-default rounded">
|
||||
<div className="text-xs text-text-muted mb-1">Selected Drone</div>
|
||||
<div className="font-mono text-sm text-text-primary">
|
||||
{selectedDrone.hostname}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted truncate">
|
||||
{selectedDrone.workspaceDir}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-text-muted truncate">
|
||||
{selectedDrone.workspaceDir}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">
|
||||
Session Name (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
|
||||
placeholder="Auto-generated from first prompt"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">
|
||||
AI Provider *
|
||||
</label>
|
||||
<select
|
||||
value={selectedProviderId}
|
||||
onChange={(e) => {
|
||||
setSelectedProviderId(e.target.value);
|
||||
setSelectedModel("");
|
||||
}}
|
||||
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
|
||||
>
|
||||
<option value="">Select a provider</option>
|
||||
{providers.map((provider) => (
|
||||
<option key={provider._id} value={provider._id}>
|
||||
{provider.name} ({provider.apiType})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedProvider && (
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">
|
||||
Model *
|
||||
Session Name (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
|
||||
placeholder="Auto-generated from first prompt"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">
|
||||
AI Provider *
|
||||
</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
value={selectedProviderId}
|
||||
onChange={(e) => {
|
||||
setSelectedProviderId(e.target.value);
|
||||
setSelectedModel("");
|
||||
}}
|
||||
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
|
||||
disabled={selectedProvider.models.length === 0}
|
||||
>
|
||||
<option value="">Select a model</option>
|
||||
{[...selectedProvider.models].sort((a, b) => a.name.localeCompare(b.name)).map((model) => (
|
||||
<option key={model.id} value={model.id}>
|
||||
{model.name}{" "}
|
||||
{model.parameterLabel ? `(${model.parameterLabel})` : ""}
|
||||
<option value="">Select a provider</option>
|
||||
{providers.map((provider) => (
|
||||
<option key={provider._id} value={provider._id}>
|
||||
{provider.name} ({provider.apiType})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedProvider.models.length === 0 && (
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
No models discovered. Run `pnpm cli provider probe{" "}
|
||||
{selectedProvider._id}` to discover models.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">
|
||||
Mode
|
||||
</label>
|
||||
<select
|
||||
value={mode}
|
||||
onChange={(e) => setMode(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
|
||||
>
|
||||
<option value="plan">Plan - Planning and brainstorming</option>
|
||||
<option value="build">Build - Building and coding</option>
|
||||
<option value="test">Test - Testing and debugging</option>
|
||||
<option value="ship">Ship - Finalizing and shipping</option>
|
||||
<option value="dev">Dev - Working on Gadget Code itself</option>
|
||||
</select>
|
||||
</div>
|
||||
{selectedProvider && (
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">
|
||||
Model *
|
||||
</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
|
||||
disabled={selectedProvider.models.length === 0}
|
||||
>
|
||||
<option value="">Select a model</option>
|
||||
{[...selectedProvider.models]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((model) => (
|
||||
<option key={model.id} value={model.id}>
|
||||
{model.name}{" "}
|
||||
{model.parameterLabel
|
||||
? `(${model.parameterLabel})`
|
||||
: ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedProvider.models.length === 0 && (
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
No models discovered. Run `pnpm cli provider probe{" "}
|
||||
{selectedProvider._id}` to discover models.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={creating || !selectedProviderId || !selectedModel}
|
||||
className="px-4 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50 flex-1"
|
||||
>
|
||||
{creating ? "Creating..." : "Create Session"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 border border-border-default text-text-secondary rounded hover:bg-bg-tertiary transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">
|
||||
Mode
|
||||
</label>
|
||||
<select
|
||||
value={mode}
|
||||
onChange={(e) => setMode(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
|
||||
>
|
||||
<option value="plan">Plan - Planning and brainstorming</option>
|
||||
<option value="build">Build - Building and coding</option>
|
||||
<option value="test">Test - Testing and debugging</option>
|
||||
<option value="ship">Ship - Finalizing and shipping</option>
|
||||
<option value="dev">Dev - Working on Gadget Code itself</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={creating || !selectedProviderId || !selectedModel}
|
||||
className="px-4 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50 flex-1"
|
||||
>
|
||||
{creating ? "Creating..." : "Create Session"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 border border-border-default text-text-secondary rounded hover:bg-bg-tertiary transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -774,7 +815,9 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
|
||||
const { slug } = useParams();
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||
const [selectedDrone, setSelectedDrone] = useState<DroneRegistration | null>(null);
|
||||
const [selectedDrone, setSelectedDrone] = useState<DroneRegistration | null>(
|
||||
null,
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showNewForm, setShowNewForm] = useState(false);
|
||||
|
||||
@ -839,7 +882,10 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
|
||||
console.error("Failed to lock drone session");
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('dtp_drone_registration', JSON.stringify(selectedDrone));
|
||||
localStorage.setItem(
|
||||
"dtp_drone_registration",
|
||||
JSON.stringify(selectedDrone),
|
||||
);
|
||||
navigate(`/projects/${selectedProject._id}/chat-session/${sessionId}`);
|
||||
} catch (err) {
|
||||
console.error("Failed to open chat session", err);
|
||||
|
||||
BIN
gadget-code/frontend/src/pages/assets/gab-ai.jpeg
Normal file
BIN
gadget-code/frontend/src/pages/assets/gab-ai.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 210 KiB |
29
gadget-code/frontend/src/types/assets.d.ts
vendored
Normal file
29
gadget-code/frontend/src/types/assets.d.ts
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
declare module "*.jpeg" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module "*.jpg" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module "*.png" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module "*.gif" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module "*.svg" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module "*.webp" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
@ -337,6 +337,7 @@ class AgentService extends GadgetService {
|
||||
abortCurrentWorkOrder(): boolean {
|
||||
if (!this.abortController) return false;
|
||||
this.abortController.abort();
|
||||
AiService.abort();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ const aiEnv: IAiEnvironment = {
|
||||
import { IAiProvider as DbAiProvider, GadgetId, type IDroneModelConfig } from "@gadget/api";
|
||||
import { GadgetService } from "../lib/service.js";
|
||||
import {
|
||||
AiApi,
|
||||
type IAiChatOptions,
|
||||
type IAiChatResponse,
|
||||
type IAiGenerateOptions,
|
||||
@ -47,6 +48,8 @@ class AiService extends GadgetService {
|
||||
return "svc:ai";
|
||||
}
|
||||
|
||||
private activeApi: AiApi | null = null;
|
||||
|
||||
async start(): Promise<void> {
|
||||
this.log.info("started");
|
||||
}
|
||||
@ -103,7 +106,11 @@ class AiService extends GadgetService {
|
||||
});
|
||||
const api = this.getApi(config);
|
||||
const modelConfig: IAiModelConfig = { ...model, provider: config };
|
||||
return api.generate(modelConfig, options, streamCallback);
|
||||
try {
|
||||
return await api.generate(modelConfig, options, streamCallback);
|
||||
} finally {
|
||||
this.activeApi = null;
|
||||
}
|
||||
}
|
||||
|
||||
async chat(
|
||||
@ -121,11 +128,23 @@ class AiService extends GadgetService {
|
||||
});
|
||||
const api = this.getApi(config);
|
||||
const modelConfig: IAiModelConfig = { ...model, provider: config };
|
||||
return await api.chat(modelConfig, options, streamCallback);
|
||||
try {
|
||||
return await api.chat(modelConfig, options, streamCallback);
|
||||
} finally {
|
||||
this.activeApi = null;
|
||||
}
|
||||
}
|
||||
|
||||
getApi(provider: AiProviderConfig) {
|
||||
return createAiApi(aiEnv, provider, this.log);
|
||||
const api = createAiApi(aiEnv, provider, this.log);
|
||||
this.activeApi = api;
|
||||
return api;
|
||||
}
|
||||
|
||||
/** Forcefully abort any in-progress request on the active API instance. */
|
||||
abort(): void {
|
||||
this.activeApi?.abort();
|
||||
this.activeApi = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -196,6 +196,15 @@ export abstract class AiApi {
|
||||
streamCallback?: IAiResponseStreamFn,
|
||||
): Promise<IAiChatResponse>;
|
||||
|
||||
/**
|
||||
* Forcefully abort any in-progress API request.
|
||||
* Provider-specific implementations (e.g., Ollama) should terminate
|
||||
* the underlying HTTP request immediately. Default is no-op.
|
||||
*/
|
||||
abort(): void {
|
||||
// Override in provider-specific implementations
|
||||
}
|
||||
|
||||
protected assertNonEmptyChatResponse(response: IAiChatResponse): void {
|
||||
const hasResponse = response.response.trim().length > 0;
|
||||
const hasThinking = !!response.thinking?.trim();
|
||||
|
||||
@ -34,6 +34,11 @@ export class OllamaAiApi extends AiApi {
|
||||
});
|
||||
}
|
||||
|
||||
/** Forcefully abort any in-progress Ollama API request. */
|
||||
override abort(): void {
|
||||
this.client.abort();
|
||||
}
|
||||
|
||||
async listModels(): Promise<IAiModelListResult> {
|
||||
const response = await this.client.list();
|
||||
const models = response.models.map((model) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user