make reasoning effort configurable; remove sign up concept
- Implemented reasoning effort setting in SESSION panel of Chat Sessio View - Removed all ability to "sign up" for an account
This commit is contained in:
parent
63e812b7c3
commit
11bdd5e3b0
@ -147,11 +147,11 @@ email:
|
||||
enabled: true
|
||||
maxConnections: 5
|
||||
maxMessages: 100
|
||||
contact:
|
||||
to: "Support <support@example.com>"
|
||||
contact:
|
||||
to: "Support <support@example.com>"
|
||||
|
||||
# MinIO/S3 configuration (optional)
|
||||
minio:
|
||||
# MinIO/S3 configuration (optional)
|
||||
minio:
|
||||
endpoint: "localhost"
|
||||
port: 9000
|
||||
useSsl: false
|
||||
@ -161,14 +161,10 @@ minio:
|
||||
uploads: "gadget-uploads"
|
||||
images: "gadget-images"
|
||||
videos: "gadget-videos"
|
||||
audios: "gadget-audios"
|
||||
audios: "gadget-audios"
|
||||
```
|
||||
|
||||
# User settings
|
||||
user:
|
||||
signupEnabled: false
|
||||
```
|
||||
|
||||
## gadget-drone.yaml Reference
|
||||
## gadget-drone.yaml Reference
|
||||
|
||||
```yaml
|
||||
# Basic settings
|
||||
|
||||
171
docs/reasoning-effort.md
Normal file
171
docs/reasoning-effort.md
Normal file
@ -0,0 +1,171 @@
|
||||
# Reasoning Effort
|
||||
|
||||
**Status:** ✅ **IMPLEMENTED**
|
||||
**Last Updated:** May 8, 2026
|
||||
|
||||
## Overview
|
||||
|
||||
Reasoning effort controls how much an AI model "thinks" before responding. Models with reasoning capabilities (like DeepSeek-R1, QwQ, OpenAI o1/o3) can produce internal chain-of-thought tokens before generating their final answer. The reasoning effort setting lets users balance between speed and thoroughness.
|
||||
|
||||
## User Setting
|
||||
|
||||
The reasoning effort is configured per chat session via a dropdown in the Session sidebar:
|
||||
|
||||
| Value | Effect |
|
||||
|----------|-----------------------------------------------------|
|
||||
| **Off** | No thinking output. Model responds immediately. |
|
||||
| **Low** | Minimal thinking. Faster responses, less depth. |
|
||||
| **Medium** | Balanced thinking. Default reasoning depth. |
|
||||
| **High** | Maximum thinking. Slower but more thorough. |
|
||||
|
||||
The dropdown is **disabled** when the selected model does not have `hasThinking: true` in its capabilities.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User selects "High" in Reasoning dropdown
|
||||
→ PUT /api/v1/chat-sessions/:id { reasoningEffort: "high" }
|
||||
→ Stored in MongoDB ChatSession.reasoningEffort
|
||||
→ When creating a turn:
|
||||
ChatTurn.reasoningEffort = ChatSession.reasoningEffort (snapshotted)
|
||||
→ Drone receives work order with populated turn
|
||||
→ agent.ts reads turn.reasoningEffort, maps "off" → false
|
||||
→ Passes to AiService.chat() as params.reasoning
|
||||
→ Provider SDK receives the appropriate parameter
|
||||
```
|
||||
|
||||
## Provider Mapping
|
||||
|
||||
Each AI provider uses a different parameter name for reasoning effort. The `@gadget/ai` abstraction handles the translation:
|
||||
|
||||
| Provider | Parameter | Values |
|
||||
|----------|------------------|---------------------------------|
|
||||
| Ollama | `think` | `false`, `"low"`, `"medium"`, `"high"` |
|
||||
| OpenAI | `reasoning_effort` | `"low"`, `"medium"`, `"high"` |
|
||||
|
||||
### Mapping Logic (in `gadget-drone/src/services/agent.ts`)
|
||||
|
||||
```typescript
|
||||
const reasoningEffort = turn.reasoningEffort || "off";
|
||||
const reasoning: boolean | "low" | "medium" | "high" =
|
||||
reasoningEffort === "off" ? false : reasoningEffort;
|
||||
```
|
||||
|
||||
- `"off"` → `false` (disables thinking entirely)
|
||||
- `"low"` → `"low"` (minimal thinking)
|
||||
- `"medium"` → `"medium"` (balanced)
|
||||
- `"high"` → `"high"` (maximum thinking)
|
||||
|
||||
### Ollama Implementation (`packages/ai/src/ollama.ts`)
|
||||
|
||||
```typescript
|
||||
const response = await this.client.chat({
|
||||
model: model.modelId,
|
||||
messages,
|
||||
stream: true,
|
||||
think: model.params.reasoning, // boolean | "low" | "medium" | "high"
|
||||
});
|
||||
```
|
||||
|
||||
When `think` is `false`, the Ollama SDK disables thinking. When set to a string level, the model allocates corresponding effort.
|
||||
|
||||
### OpenAI Implementation (`packages/ai/src/openai.ts`)
|
||||
|
||||
```typescript
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: model.modelId,
|
||||
messages,
|
||||
tools,
|
||||
stream: true,
|
||||
...(typeof model.params.reasoning === "string"
|
||||
? { reasoning_effort: model.params.reasoning }
|
||||
: {}),
|
||||
});
|
||||
```
|
||||
|
||||
The `reasoning_effort` parameter is only passed when the value is a string (`"low"`, `"medium"`, `"high"`). When `false`, the parameter is omitted — standard non-reasoning models would reject it.
|
||||
|
||||
## Streaming Thinking Content
|
||||
|
||||
When reasoning effort is enabled and the model produces thinking tokens, they are streamed back in real-time:
|
||||
|
||||
1. **Provider SDK** emits thinking tokens in stream chunks
|
||||
2. **Provider implementation** (`ollama.ts` / `openai.ts`) maps them to `IAiStreamChunk` with `type: 'thinking'`
|
||||
3. **Drone** forwards via Socket.IO as `thinking(content)` events
|
||||
4. **Frontend** renders thinking content in distinct muted blocks
|
||||
|
||||
### Thinking Chunk Handling
|
||||
|
||||
**Ollama:**
|
||||
```typescript
|
||||
if (chunk.message.thinking) {
|
||||
await streamCallback({
|
||||
type: 'thinking',
|
||||
data: chunk.message.thinking,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**OpenAI:**
|
||||
```typescript
|
||||
if ('reasoning' in delta && delta.reasoning) {
|
||||
await streamCallback({
|
||||
type: 'thinking',
|
||||
data: delta.reasoning as string,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Type Definitions
|
||||
|
||||
### `ReasoningEffort` (in `packages/api/src/interfaces/chat-session.ts`)
|
||||
|
||||
```typescript
|
||||
export type ReasoningEffort = "off" | "low" | "medium" | "high";
|
||||
```
|
||||
|
||||
### `IAiModelConfig.params.reasoning` (in `packages/ai/src/api.ts`)
|
||||
|
||||
```typescript
|
||||
params: {
|
||||
reasoning: boolean | "high" | "medium" | "low";
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Note: The `IAiModelConfig` type uses `boolean | "high" | "medium" | "low"` (no `"off"`). The `"off"` value from the user-facing setting is mapped to `false` before reaching the AI provider layer.
|
||||
|
||||
### Mongoose Schema
|
||||
|
||||
**ChatSession** (`gadget-code/src/models/chat-session.ts`):
|
||||
```typescript
|
||||
reasoningEffort: {
|
||||
type: String,
|
||||
enum: ["off", "low", "medium", "high"],
|
||||
default: "off",
|
||||
}
|
||||
```
|
||||
|
||||
**ChatTurn** (`gadget-code/src/models/chat-turn.ts`):
|
||||
```typescript
|
||||
reasoningEffort: {
|
||||
type: String,
|
||||
enum: ["off", "low", "medium", "high"],
|
||||
default: "off",
|
||||
}
|
||||
```
|
||||
|
||||
## Model Capability Detection
|
||||
|
||||
The `hasThinking` capability is detected during model probing:
|
||||
|
||||
- **Ollama**: checks if model capabilities array includes `"reasoning"`
|
||||
- **OpenAI**: checks if model features include `"reasoning_effort"` or fallback detection by model ID (`o1`, `o3`, `reasoning`)
|
||||
|
||||
The frontend uses this capability flag to enable/disable the Reasoning dropdown.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Streaming Responses](./streaming-responses.md) — How thinking tokens are streamed to the IDE
|
||||
- [Socket Protocol](./socket-protocol.md) — Socket.IO event definitions
|
||||
- [Architecture](./architecture.md) — Overall system architecture
|
||||
@ -426,6 +426,15 @@ marked.setOptions({
|
||||
- Thinking and response mixed in same block (mode detection broken)
|
||||
- Tool calls not appearing (tool call events not routed)
|
||||
|
||||
## Reasoning Effort
|
||||
|
||||
The reasoning effort setting controls how much a model thinks before responding. See [Reasoning Effort](./reasoning-effort.md) for full documentation.
|
||||
|
||||
Key integration points with streaming:
|
||||
- When reasoning effort is **Off** (`false`), no thinking tokens are produced
|
||||
- When set to **Low/Medium/High**, the model allocates corresponding thinking depth
|
||||
- Thinking tokens stream through the same path as response tokens but with `type: 'thinking'`
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements not yet implemented:
|
||||
@ -477,3 +486,4 @@ Potential improvements not yet implemented:
|
||||
- [Socket Protocol](./socket-protocol.md) — Socket.IO event definitions
|
||||
- [ChatTurn Interface](../packages/api/src/interfaces/chat-turn.ts) — TypeScript types
|
||||
- [ChatTurn Model](../gadget-code/src/models/chat-turn.ts) — Mongoose schema
|
||||
- [Reasoning Effort](./reasoning-effort.md) — How thinking/reasoning is controlled
|
||||
|
||||
@ -112,18 +112,15 @@ DTP_HTTPS_PORT="3443"
|
||||
DTP_HTTPS_KEY_FILE="/path/to/ssl/key.pem"
|
||||
DTP_HTTPS_CRT_FILE="/path/to/ssl/cert.pem"
|
||||
|
||||
# Session
|
||||
DTP_SESSION_TRUST_PROXY="enabled"
|
||||
DTP_SESSION_COOKIE_SECURE="enabled"
|
||||
DTP_SESSION_COOKIE_SAMESITE="strict"
|
||||
|
||||
# User Signup
|
||||
DTP_USER_SIGNUP="enabled"
|
||||
```
|
||||
# Session
|
||||
DTP_SESSION_TRUST_PROXY="enabled"
|
||||
DTP_SESSION_COOKIE_SECURE="enabled"
|
||||
DTP_SESSION_COOKIE_SAMESITE="strict"
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- User authentication (sign up, sign in, sign out)
|
||||
- User authentication (sign in, sign out)
|
||||
- JWT-based session management
|
||||
- RESTful API structure
|
||||
- Socket.io real-time communication
|
||||
@ -153,12 +150,11 @@ DTP_USER_SIGNUP="enabled"
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
| ------ | ---------------- | ------------------ |
|
||||
| POST | `/auth/sign-up` | Create new account |
|
||||
| POST | `/auth/sign-in` | Authenticate |
|
||||
| GET | `/auth/sign-out` | Sign out |
|
||||
| GET | `/api/v1/user` | Get current user |
|
||||
| Method | Endpoint | Description |
|
||||
| ------ | ---------------- | ------------------ |
|
||||
| POST | `/auth/sign-in` | Authenticate |
|
||||
| GET | `/auth/sign-out` | Sign out |
|
||||
| GET | `/api/v1/user` | Get current user |
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@ -7,7 +7,6 @@ import StatusBar from './components/StatusBar';
|
||||
import Home from './pages/Home';
|
||||
import ProjectManager from './pages/ProjectManager';
|
||||
import SignIn from './pages/SignIn';
|
||||
import SignUp from './pages/SignUp';
|
||||
import ChatSessionView from './pages/ChatSessionView';
|
||||
import DroneManager from './pages/DroneManager';
|
||||
|
||||
@ -124,27 +123,17 @@ export default function App() {
|
||||
<Route path="/projects/:slug" element={<ProjectManager user={user} />} />
|
||||
<Route path="/drones" element={<DroneManager user={user} />} />
|
||||
<Route path="/projects/:projectId/chat-session/:sessionId" element={<ChatSessionView />} />
|
||||
<Route
|
||||
path="/sign-in"
|
||||
element={
|
||||
user ? (
|
||||
<Navigate to="/" replace />
|
||||
) : (
|
||||
<SignIn onSuccess={handleSignInSuccess} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/sign-up"
|
||||
element={
|
||||
user ? (
|
||||
<Navigate to="/" replace />
|
||||
) : (
|
||||
<SignUp onSuccess={handleSignInSuccess} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<Route
|
||||
path="/sign-in"
|
||||
element={
|
||||
user ? (
|
||||
<Navigate to="/" replace />
|
||||
) : (
|
||||
<SignIn onSuccess={handleSignInSuccess} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
<StatusBar statusMessage={statusMessage} projectSlug={currentProject} />
|
||||
</div>
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface User {
|
||||
_id: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
interface NavbarProps {
|
||||
user: User | null;
|
||||
onSignOut: () => void;
|
||||
}
|
||||
|
||||
export default function Navbar({ user, onSignOut }: NavbarProps) {
|
||||
return (
|
||||
<nav className="bg-bg-secondary border-b border-border">
|
||||
<div className="max-w-4xl mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<Link to="/" className="text-xl font-semibold text-text">
|
||||
DTP Web App
|
||||
</Link>
|
||||
<div className="flex items-center gap-4">
|
||||
{user ? (
|
||||
<>
|
||||
<span className="text-text-muted">{user.displayName}</span>
|
||||
<button
|
||||
onClick={onSignOut}
|
||||
className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg transition-colors"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
to="/sign-in"
|
||||
className="px-4 py-2 text-text-muted hover:text-text transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
<Link
|
||||
to="/sign-up"
|
||||
className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg transition-colors"
|
||||
>
|
||||
Sign Up
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@ -280,6 +280,7 @@ export interface ChatSession {
|
||||
mode: ChatSessionMode;
|
||||
provider: string | AiProvider;
|
||||
selectedModel: string;
|
||||
reasoningEffort?: string;
|
||||
stats: ChatSessionStats;
|
||||
pins: Array<{ _id?: string; content: string }>;
|
||||
}
|
||||
|
||||
@ -40,12 +40,14 @@ export default function ChatSessionView() {
|
||||
const [isUpdatingMode, setIsUpdatingMode] = useState(false);
|
||||
const [isUpdatingProvider, setIsUpdatingProvider] = useState(false);
|
||||
const [isUpdatingModel, setIsUpdatingModel] = useState(false);
|
||||
const [isUpdatingReasoning, setIsUpdatingReasoning] = useState(false);
|
||||
const [logExpanded, setLogExpanded] = useState(false);
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
const [toast, setToast] = useState<string | null>(null);
|
||||
const [providers, setProviders] = useState<AiProvider[]>([]);
|
||||
const [selectedProviderId, setSelectedProviderId] = useState<string>('');
|
||||
const [selectedModelId, setSelectedModelId] = useState<string>('');
|
||||
const [sessionReasoningEffort, setSessionReasoningEffort] = useState<string>('off');
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
@ -104,6 +106,7 @@ export default function ChatSessionView() {
|
||||
: sessionData.provider?._id;
|
||||
setSelectedProviderId(providerId || '');
|
||||
setSelectedModelId(sessionData.selectedModel || '');
|
||||
setSessionReasoningEffort(sessionData.reasoningEffort || 'off');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load session');
|
||||
@ -455,6 +458,34 @@ export default function ChatSessionView() {
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectedModelCapabilities = useCallback(() => {
|
||||
const provider = providers.find(p => p._id === selectedProviderId);
|
||||
if (!provider) return null;
|
||||
const model = provider.models.find(m => m.id === selectedModelId);
|
||||
return model?.capabilities || null;
|
||||
}, [providers, selectedProviderId, selectedModelId]);
|
||||
|
||||
const handleReasoningChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (!session) return;
|
||||
|
||||
const newValue = e.target.value;
|
||||
if (newValue === sessionReasoningEffort) return;
|
||||
|
||||
setIsUpdatingReasoning(true);
|
||||
try {
|
||||
const updatedSession = await chatSessionApi.update(session._id, {
|
||||
reasoningEffort: newValue
|
||||
});
|
||||
setSession(updatedSession);
|
||||
setSessionReasoningEffort(newValue);
|
||||
showToast(`Reasoning effort set to ${newValue}`);
|
||||
} catch (err) {
|
||||
showToast(`Failed to change reasoning effort: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
} finally {
|
||||
setIsUpdatingReasoning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (!session) return;
|
||||
|
||||
@ -731,6 +762,25 @@ export default function ChatSessionView() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-text-muted">Reasoning</div>
|
||||
<select
|
||||
value={sessionReasoningEffort}
|
||||
onChange={handleReasoningChange}
|
||||
disabled={isUpdatingReasoning || !getSelectedModelCapabilities()?.hasThinking}
|
||||
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title={
|
||||
!getSelectedModelCapabilities()?.hasThinking
|
||||
? "Selected model does not support reasoning"
|
||||
: "Controls how much the model thinks before responding"
|
||||
}
|
||||
>
|
||||
<option value="off">Off</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -77,14 +77,8 @@ export default function SignIn({ onSuccess }: SignInProps) {
|
||||
{loading ? 'Signing in...' : 'Sign In'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p className="mt-4 text-center text-text-muted">
|
||||
Don't have an account?{' '}
|
||||
<Link to="/sign-up" className="text-brand hover:underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,122 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { api, AuthResponse, User } from '../lib/api';
|
||||
|
||||
interface SignUpProps {
|
||||
onSuccess: (user: User, token: string) => void;
|
||||
}
|
||||
|
||||
export default function SignUp({ onSuccess }: SignUpProps) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordVerify, setPasswordVerify] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (password !== passwordVerify) {
|
||||
setError('Passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.post<AuthResponse>('/auth/sign-up', {
|
||||
email,
|
||||
password,
|
||||
displayName,
|
||||
});
|
||||
onSuccess(response.user, response.token);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Sign up failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-bg-primary">
|
||||
<div className="w-full max-w-md p-8">
|
||||
<h1 className="text-2xl font-bold text-center mb-6">Create Account</h1>
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-900/30 border border-red-700 rounded-lg text-red-200">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm text-text-muted mb-1">
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-brand"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="displayName" className="block text-sm text-text-muted mb-1">
|
||||
Display Name
|
||||
</label>
|
||||
<input
|
||||
id="displayName"
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-brand"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm text-text-muted mb-1">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-brand"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="passwordVerify" className="block text-sm text-text-muted mb-1">
|
||||
Verify Password
|
||||
</label>
|
||||
<input
|
||||
id="passwordVerify"
|
||||
type="password"
|
||||
value={passwordVerify}
|
||||
onChange={(e) => setPasswordVerify(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-brand"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4 pt-2">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex-1 px-4 py-2 text-center border border-border-default rounded-lg hover:bg-bg-tertiary transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</Link>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex-1 px-4 py-2 bg-brand hover:bg-red-700 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Creating...' : 'Sign Up'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -47,6 +47,21 @@ export default defineConfig({
|
||||
build: {
|
||||
outDir: path.join(rootDir, 'dist', 'client'),
|
||||
emptyOutDir: true,
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
codeSplitting: {
|
||||
minSize: 20000,
|
||||
groups: [
|
||||
{
|
||||
name: 'vendor',
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
priority: 10,
|
||||
maxSize: 250000,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@ -124,10 +124,9 @@ export default {
|
||||
audios: yamlConfig.minio?.buckets?.audios || "dtp-audios",
|
||||
},
|
||||
},
|
||||
user: {
|
||||
signupEnabled: yamlConfig.user?.signupEnabled === true,
|
||||
passwordSalt: yamlConfig.auth.passwordSalt,
|
||||
},
|
||||
user: {
|
||||
passwordSalt: yamlConfig.auth.passwordSalt,
|
||||
},
|
||||
https: {
|
||||
enabled: yamlConfig.https?.enabled === true,
|
||||
address: yamlConfig.https?.address || "127.0.0.1",
|
||||
|
||||
@ -178,6 +178,7 @@ class ChatSessionController extends DtpController {
|
||||
provider: string;
|
||||
selectedModel: string;
|
||||
mode: ChatSessionMode;
|
||||
reasoningEffort: "off" | "low" | "medium" | "high";
|
||||
}> = {};
|
||||
|
||||
if (updates.name !== undefined) {
|
||||
@ -196,6 +197,13 @@ class ChatSessionController extends DtpController {
|
||||
throw new Error(`Invalid mode: ${updates.mode}`);
|
||||
}
|
||||
}
|
||||
if (updates.reasoningEffort !== undefined) {
|
||||
const validValues = ["off", "low", "medium", "high"] as const;
|
||||
if (!validValues.includes(updates.reasoningEffort)) {
|
||||
throw new Error(`Invalid reasoningEffort: ${updates.reasoningEffort}. Must be one of: ${validValues.join(", ")}`);
|
||||
}
|
||||
allowedUpdates.reasoningEffort = updates.reasoningEffort;
|
||||
}
|
||||
|
||||
const session = await ChatSessionService.update(
|
||||
res.locals.chatSession._id,
|
||||
|
||||
@ -26,60 +26,14 @@ export class AuthController extends DtpController {
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
this.router.post("/sign-up", this.postSignUp.bind(this));
|
||||
this.router.post("/sign-in", this.postSignIn.bind(this));
|
||||
this.router.post("/renew-token", this.postRenewToken.bind(this));
|
||||
|
||||
this.router.get("/welcome", this.getWelcomeView.bind(this));
|
||||
this.router.get("/sign-up", this.getSignUpForm.bind(this));
|
||||
this.router.get("/sign-in", this.getSignInForm.bind(this));
|
||||
this.router.get("/sign-out", this.getSignOut.bind(this));
|
||||
}
|
||||
|
||||
async postSignUp(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
try {
|
||||
const user = await UserService.create(
|
||||
req.body.email,
|
||||
req.body.password,
|
||||
req.body.displayName
|
||||
);
|
||||
|
||||
req.session.user = {
|
||||
_id: user._id,
|
||||
email: user.email,
|
||||
displayName: user.displayName,
|
||||
flags: user.flags,
|
||||
};
|
||||
|
||||
const token = await SessionService.createJsonWebToken(user);
|
||||
req.session.token = token;
|
||||
req.session.type = SessionType.WEB;
|
||||
|
||||
req.session.save((err: Error) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
user: {
|
||||
_id: user._id.toString(),
|
||||
email: user.email,
|
||||
displayName: user.displayName,
|
||||
flags: user.flags,
|
||||
},
|
||||
token,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
this.log.error("failed to process new user sign-up", { error });
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async postSignIn(
|
||||
req: Request,
|
||||
res: Response,
|
||||
@ -143,12 +97,6 @@ export class AuthController extends DtpController {
|
||||
});
|
||||
}
|
||||
|
||||
async getSignUpForm(_req: Request, res: Response): Promise<void> {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
form: "sign-up",
|
||||
});
|
||||
}
|
||||
async getSignInForm(_req: Request, res: Response): Promise<void> {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
|
||||
@ -68,12 +68,11 @@ export abstract class DtpController implements DtpComponent {
|
||||
* @param res Response The response being generated.
|
||||
* @param next NextFunction The next function to call when done.
|
||||
*/
|
||||
middleware(req: Request, res: Response, next: NextFunction) {
|
||||
res.locals.request = req;
|
||||
res.locals.currentView = this.slug;
|
||||
res.locals.signupEnabled = env.user.signupEnabled;
|
||||
next();
|
||||
}
|
||||
middleware(req: Request, res: Response, next: NextFunction) {
|
||||
res.locals.request = req;
|
||||
res.locals.currentView = this.slug;
|
||||
next();
|
||||
}
|
||||
|
||||
hmacMiddleware() {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
@ -25,6 +25,11 @@ export const ChatSessionSchema = new Schema<IChatSession>({
|
||||
},
|
||||
provider: { type: String, required: true, ref: "AiProvider" },
|
||||
selectedModel: { type: String, required: true },
|
||||
reasoningEffort: {
|
||||
type: String,
|
||||
enum: ["off", "low", "medium", "high"],
|
||||
default: "off",
|
||||
},
|
||||
stats: {
|
||||
turnCount: { type: Number, default: 0, required: true },
|
||||
toolCallCount: { type: Number, default: 0, required: true },
|
||||
|
||||
@ -63,6 +63,11 @@ export const ChatTurnSchema = new Schema<IChatTurn>({
|
||||
session: { type: String, required: true, ref: "ChatSession" },
|
||||
provider: { type: String, required: true, ref: "AiProvider" },
|
||||
llm: { type: String, required: true }, // id/name of the model used to process the prompt
|
||||
reasoningEffort: {
|
||||
type: String,
|
||||
enum: ["off", "low", "medium", "high"],
|
||||
default: "off",
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
enum: ChatSessionMode,
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
IProject,
|
||||
ChatTurnStatus,
|
||||
ChatTurnDocument,
|
||||
ReasoningEffort,
|
||||
} from "@gadget/api";
|
||||
|
||||
import { DtpService } from "../lib/service.js";
|
||||
@ -178,6 +179,7 @@ class ChatSessionService extends DtpService {
|
||||
provider: GadgetId;
|
||||
selectedModel: string;
|
||||
mode: ChatSessionMode;
|
||||
reasoningEffort: ReasoningEffort;
|
||||
}>,
|
||||
): Promise<IChatSession> {
|
||||
const session = await ChatSession.findById(chatSessionId);
|
||||
@ -203,6 +205,9 @@ class ChatSessionService extends DtpService {
|
||||
if (updates.mode !== undefined) {
|
||||
session.mode = updates.mode;
|
||||
}
|
||||
if (updates.reasoningEffort !== undefined) {
|
||||
session.reasoningEffort = updates.reasoningEffort;
|
||||
}
|
||||
|
||||
await session.save();
|
||||
|
||||
@ -248,6 +253,7 @@ class ChatSessionService extends DtpService {
|
||||
session: session._id,
|
||||
provider: session.provider,
|
||||
llm: session.selectedModel,
|
||||
reasoningEffort: (session as IChatSession).reasoningEffort || "off",
|
||||
mode: session.mode,
|
||||
status: ChatTurnStatus.Processing,
|
||||
prompts: {
|
||||
|
||||
@ -150,7 +150,7 @@ describe('Environment Configuration', () => {
|
||||
const content = fs.readFileSync(envPath, 'utf-8');
|
||||
|
||||
expect(content).toContain('https');
|
||||
expect(content).toContain('port: parseInt');
|
||||
expect(content).toContain('port: yamlConfig.https?.port');
|
||||
expect(content).toContain('keyFile');
|
||||
expect(content).toContain('crtFile');
|
||||
});
|
||||
|
||||
@ -46,20 +46,22 @@ describe("CodeSession", () => {
|
||||
status: "available",
|
||||
} as IDroneRegistration;
|
||||
|
||||
mockProject = {
|
||||
_id: nanoid(),
|
||||
slug: "test-project",
|
||||
name: "Test Project",
|
||||
} as IProject;
|
||||
|
||||
mockChatSession = {
|
||||
_id: nanoid(),
|
||||
name: "Test Session",
|
||||
mode: "build",
|
||||
provider: nanoid(),
|
||||
selectedModel: "llama3.1",
|
||||
user: mockUser,
|
||||
project: mockProject,
|
||||
} as IChatSession;
|
||||
|
||||
mockProject = {
|
||||
_id: nanoid(),
|
||||
slug: "test-project",
|
||||
name: "Test Project",
|
||||
} as IProject;
|
||||
|
||||
codeSession = new CodeSession(mockSocket, mockUser);
|
||||
});
|
||||
|
||||
@ -81,29 +83,32 @@ describe("CodeSession", () => {
|
||||
});
|
||||
|
||||
describe("onSubmitPrompt", () => {
|
||||
it("should throw error if no drone is selected", async () => {
|
||||
it("should return error if no drone is selected", async () => {
|
||||
codeSession.setChatSession(mockChatSession, mockProject);
|
||||
const cb = vi.fn();
|
||||
|
||||
await expect(codeSession.onSubmitPrompt("test prompt")).rejects.toThrow(
|
||||
"No drone selected",
|
||||
);
|
||||
await codeSession.onSubmitPrompt("test prompt", cb);
|
||||
|
||||
expect(cb).toHaveBeenCalledWith(false, { message: "No drone selected" });
|
||||
});
|
||||
|
||||
it("should throw error if no chat session is active", async () => {
|
||||
it("should return error if no chat session is active", async () => {
|
||||
codeSession.setSelectedDrone(mockDrone);
|
||||
const cb = vi.fn();
|
||||
|
||||
await expect(codeSession.onSubmitPrompt("test prompt")).rejects.toThrow(
|
||||
"No chat session active",
|
||||
);
|
||||
await codeSession.onSubmitPrompt("test prompt", cb);
|
||||
|
||||
expect(cb).toHaveBeenCalledWith(false, { message: "No chat session active" });
|
||||
});
|
||||
|
||||
it("should throw error if no project is selected", async () => {
|
||||
it("should return error if no project is selected", async () => {
|
||||
codeSession.setSelectedDrone(mockDrone);
|
||||
codeSession.setChatSession(mockChatSession, undefined as any);
|
||||
const cb = vi.fn();
|
||||
|
||||
await expect(codeSession.onSubmitPrompt("test prompt")).rejects.toThrow(
|
||||
"No project selected",
|
||||
);
|
||||
await codeSession.onSubmitPrompt("test prompt", cb);
|
||||
|
||||
expect(cb).toHaveBeenCalledWith(false, { message: "No project selected" });
|
||||
});
|
||||
|
||||
it("should create a ChatTurn and emit processWorkOrder to drone", async () => {
|
||||
@ -117,6 +122,9 @@ describe("CodeSession", () => {
|
||||
vi.mocked(ChatTurn).mockImplementation(function () {
|
||||
return mockTurn as any;
|
||||
});
|
||||
(vi.mocked(ChatTurn) as any).populate = vi
|
||||
.fn()
|
||||
.mockResolvedValue(mockTurn);
|
||||
|
||||
const mockDroneSession = {
|
||||
socket: {
|
||||
@ -129,7 +137,8 @@ describe("CodeSession", () => {
|
||||
mockDroneSession as any,
|
||||
);
|
||||
|
||||
await codeSession.onSubmitPrompt("test prompt");
|
||||
const cb = vi.fn();
|
||||
await codeSession.onSubmitPrompt("test prompt", cb);
|
||||
|
||||
expect(ChatTurn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@ -139,10 +148,9 @@ describe("CodeSession", () => {
|
||||
provider: mockChatSession.provider,
|
||||
llm: mockChatSession.selectedModel,
|
||||
status: ChatTurnStatus.Processing,
|
||||
prompts: {
|
||||
prompts: expect.objectContaining({
|
||||
user: "test prompt",
|
||||
system: undefined,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@ -162,12 +170,15 @@ describe("CodeSession", () => {
|
||||
const mockTurn = {
|
||||
_id: nanoid(),
|
||||
status: ChatTurnStatus.Processing,
|
||||
response: "",
|
||||
errorMessage: "",
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
vi.mocked(ChatTurn).mockImplementation(function () {
|
||||
return mockTurn as any;
|
||||
});
|
||||
(vi.mocked(ChatTurn) as any).populate = vi
|
||||
.fn()
|
||||
.mockResolvedValue(mockTurn);
|
||||
|
||||
const mockDroneSession = {
|
||||
socket: {
|
||||
@ -183,10 +194,11 @@ describe("CodeSession", () => {
|
||||
mockDroneSession as any,
|
||||
);
|
||||
|
||||
await codeSession.onSubmitPrompt("test prompt");
|
||||
const cb = vi.fn();
|
||||
await codeSession.onSubmitPrompt("test prompt", cb);
|
||||
|
||||
expect(mockTurn.status).toBe(ChatTurnStatus.Error);
|
||||
expect(mockTurn.response).toBe("Drone is busy");
|
||||
expect(mockTurn.errorMessage).toBe("Drone is busy");
|
||||
expect(mockTurn.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -100,30 +100,25 @@ describe("DroneSession", () => {
|
||||
});
|
||||
|
||||
describe("onThinking", () => {
|
||||
it("should route thinking event to code session and update ChatTurn", async () => {
|
||||
it("should route thinking event to code session", async () => {
|
||||
const mockCodeSession = {
|
||||
socket: { emit: vi.fn() },
|
||||
onThinking: vi.fn(),
|
||||
};
|
||||
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(
|
||||
mockCodeSession as any,
|
||||
);
|
||||
vi.mocked(ChatTurn.findByIdAndUpdate).mockResolvedValue({} as any);
|
||||
|
||||
droneSession.setChatSessionId(mockChatSessionId);
|
||||
droneSession.setCurrentTurnId(mockTurnId);
|
||||
|
||||
await droneSession.onThinking("thinking content");
|
||||
|
||||
expect(SocketService.getCodeSessionByChatSessionId).toHaveBeenCalledWith(
|
||||
mockChatSessionId,
|
||||
);
|
||||
expect(mockCodeSession.socket.emit).toHaveBeenCalledWith(
|
||||
"thinking",
|
||||
expect(mockCodeSession.onThinking).toHaveBeenCalledWith(
|
||||
"thinking content",
|
||||
);
|
||||
expect(ChatTurn.findByIdAndUpdate).toHaveBeenCalledWith(mockTurnId, {
|
||||
thinking: "thinking content",
|
||||
});
|
||||
});
|
||||
|
||||
it("should log warning if no chat session is active", async () => {
|
||||
@ -134,30 +129,25 @@ describe("DroneSession", () => {
|
||||
});
|
||||
|
||||
describe("onResponse", () => {
|
||||
it("should route response event to code session and update ChatTurn", async () => {
|
||||
it("should route response event to code session", async () => {
|
||||
const mockCodeSession = {
|
||||
socket: { emit: vi.fn() },
|
||||
onResponse: vi.fn(),
|
||||
};
|
||||
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(
|
||||
mockCodeSession as any,
|
||||
);
|
||||
vi.mocked(ChatTurn.findByIdAndUpdate).mockResolvedValue({} as any);
|
||||
|
||||
droneSession.setChatSessionId(mockChatSessionId);
|
||||
droneSession.setCurrentTurnId(mockTurnId);
|
||||
|
||||
await droneSession.onResponse("response content");
|
||||
|
||||
expect(SocketService.getCodeSessionByChatSessionId).toHaveBeenCalledWith(
|
||||
mockChatSessionId,
|
||||
);
|
||||
expect(mockCodeSession.socket.emit).toHaveBeenCalledWith(
|
||||
"response",
|
||||
expect(mockCodeSession.onResponse).toHaveBeenCalledWith(
|
||||
"response content",
|
||||
);
|
||||
expect(ChatTurn.findByIdAndUpdate).toHaveBeenCalledWith(mockTurnId, {
|
||||
response: "response content",
|
||||
});
|
||||
});
|
||||
|
||||
it("should log warning if no chat session is active", async () => {
|
||||
@ -171,8 +161,10 @@ describe("DroneSession", () => {
|
||||
it("should route toolCall event to code session and update ChatTurn", async () => {
|
||||
const mockCodeSession = {
|
||||
socket: { emit: vi.fn() },
|
||||
onToolCall: vi.fn(),
|
||||
};
|
||||
const mockTurn = {
|
||||
blocks: [],
|
||||
toolCalls: [],
|
||||
stats: { toolCallCount: 0 },
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
@ -195,8 +187,7 @@ describe("DroneSession", () => {
|
||||
expect(SocketService.getCodeSessionByChatSessionId).toHaveBeenCalledWith(
|
||||
mockChatSessionId,
|
||||
);
|
||||
expect(mockCodeSession.socket.emit).toHaveBeenCalledWith(
|
||||
"toolCall",
|
||||
expect(mockCodeSession.onToolCall).toHaveBeenCalledWith(
|
||||
"call-123",
|
||||
"readFile",
|
||||
'{"path":"test.ts"}',
|
||||
@ -225,6 +216,7 @@ describe("DroneSession", () => {
|
||||
it("should update ChatTurn status and emit to code session on success", async () => {
|
||||
const mockCodeSession = {
|
||||
socket: { emit: vi.fn() },
|
||||
onWorkOrderComplete: vi.fn(),
|
||||
};
|
||||
const mockTurn = {
|
||||
status: ChatTurnStatus.Processing,
|
||||
@ -242,8 +234,7 @@ describe("DroneSession", () => {
|
||||
expect(ChatTurn.findById).toHaveBeenCalledWith(mockTurnId);
|
||||
expect(mockTurn.status).toBe(ChatTurnStatus.Finished);
|
||||
expect(mockTurn.save).toHaveBeenCalled();
|
||||
expect(mockCodeSession.socket.emit).toHaveBeenCalledWith(
|
||||
"workOrderComplete",
|
||||
expect(mockCodeSession.onWorkOrderComplete).toHaveBeenCalledWith(
|
||||
mockTurnId,
|
||||
true,
|
||||
undefined,
|
||||
@ -254,10 +245,11 @@ describe("DroneSession", () => {
|
||||
it("should update ChatTurn to Error status on failure", async () => {
|
||||
const mockCodeSession = {
|
||||
socket: { emit: vi.fn() },
|
||||
onWorkOrderComplete: vi.fn(),
|
||||
};
|
||||
const mockTurn = {
|
||||
status: ChatTurnStatus.Processing,
|
||||
response: "",
|
||||
errorMessage: "",
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(
|
||||
@ -274,7 +266,7 @@ describe("DroneSession", () => {
|
||||
);
|
||||
|
||||
expect(mockTurn.status).toBe(ChatTurnStatus.Error);
|
||||
expect(mockTurn.response).toBe("Agent crashed");
|
||||
expect(mockTurn.errorMessage).toBe("Agent crashed");
|
||||
expect(mockTurn.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ test.describe('Authentication Flow', () => {
|
||||
const userData = await page.evaluate(() => localStorage.getItem('dtp_user'));
|
||||
expect(userData).toBeNull();
|
||||
|
||||
const content = await page.content();
|
||||
expect(content).toContain('Sign Up Today!');
|
||||
});
|
||||
});
|
||||
const content = await page.content();
|
||||
expect(content).toContain('Sign In');
|
||||
});
|
||||
});
|
||||
@ -5,6 +5,11 @@ import { fileURLToPath } from 'node:url';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
|
||||
@ -131,12 +131,16 @@ class AgentService extends GadgetService {
|
||||
}
|
||||
|
||||
try {
|
||||
const reasoningEffort = turn.reasoningEffort || "off";
|
||||
const reasoning: boolean | "low" | "medium" | "high" =
|
||||
reasoningEffort === "off" ? false : reasoningEffort;
|
||||
|
||||
const response = await AiService.chat(
|
||||
turn.provider,
|
||||
{
|
||||
modelId: turn.llm,
|
||||
params: {
|
||||
reasoning: false,
|
||||
reasoning,
|
||||
temperature: 0.8,
|
||||
topP: 0.9,
|
||||
topK: 40,
|
||||
|
||||
@ -36,7 +36,7 @@ export interface IDroneModelConfig {
|
||||
provider: DbAiProvider | GadgetId;
|
||||
modelId: string;
|
||||
params: {
|
||||
reasoning: boolean;
|
||||
reasoning: boolean | "low" | "medium" | "high";
|
||||
temperature: number;
|
||||
topP: number;
|
||||
topK: number;
|
||||
|
||||
@ -372,4 +372,75 @@ describe('OllamaAiApi', () => {
|
||||
expect(response.response).toBe('Here are the results');
|
||||
});
|
||||
});
|
||||
|
||||
describe('probeModel', () => {
|
||||
it('should detect thinking capability from "thinking" (Ollama convention)', async () => {
|
||||
mockOllamaClient.show.mockResolvedValue({
|
||||
capabilities: ['completion', 'vision', 'tools', 'thinking'],
|
||||
details: { family: 'gemma4' },
|
||||
model_info: {},
|
||||
modified_at: '2026-04-04T06:20:40.211Z',
|
||||
});
|
||||
|
||||
const result = await api.probeModel('gemma4:e4b');
|
||||
expect(result.capabilities.hasThinking).toBe(true);
|
||||
expect(result.capabilities.canCallTools).toBe(true);
|
||||
expect(result.capabilities.hasVision).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect thinking capability from "reasoning" (OpenAI convention)', async () => {
|
||||
mockOllamaClient.show.mockResolvedValue({
|
||||
capabilities: ['completion', 'reasoning'],
|
||||
details: { family: 'deepseek' },
|
||||
model_info: {},
|
||||
modified_at: '2026-04-04T06:20:40.211Z',
|
||||
});
|
||||
|
||||
const result = await api.probeModel('deepseek-r1');
|
||||
expect(result.capabilities.hasThinking).toBe(true);
|
||||
});
|
||||
|
||||
it('should set hasThinking false when neither thinking nor reasoning in capabilities', async () => {
|
||||
mockOllamaClient.show.mockResolvedValue({
|
||||
capabilities: ['completion'],
|
||||
details: { family: 'llama' },
|
||||
model_info: {},
|
||||
modified_at: '2026-04-04T06:20:40.211Z',
|
||||
});
|
||||
|
||||
const result = await api.probeModel('llama3.2');
|
||||
expect(result.capabilities.hasThinking).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect vision, tools, and embedding capabilities', async () => {
|
||||
mockOllamaClient.show.mockResolvedValue({
|
||||
capabilities: ['completion', 'vision', 'tools', 'embeddings'],
|
||||
details: { family: 'llama' },
|
||||
model_info: {},
|
||||
modified_at: '2026-04-04T06:20:40.211Z',
|
||||
});
|
||||
|
||||
const result = await api.probeModel('some-model');
|
||||
expect(result.capabilities.hasVision).toBe(true);
|
||||
expect(result.capabilities.canCallTools).toBe(true);
|
||||
expect(result.capabilities.hasEmbedding).toBe(true);
|
||||
});
|
||||
|
||||
it('should extract settings from Modelfile parameters', async () => {
|
||||
mockOllamaClient.show.mockResolvedValue({
|
||||
capabilities: ['completion'],
|
||||
details: { family: 'llama' },
|
||||
model_info: {},
|
||||
parameters: 'temperature 0.7\ntop_k 40\ntop_p 0.9\nnum_ctx 4096',
|
||||
modified_at: '2026-04-04T06:20:40.211Z',
|
||||
});
|
||||
|
||||
const result = await api.probeModel('llama3.2');
|
||||
expect(result.settings).toBeDefined();
|
||||
expect(result.settings!.temperature).toBe(0.7);
|
||||
expect(result.settings!.topK).toBe(40);
|
||||
expect(result.settings!.topP).toBe(0.9);
|
||||
expect(result.settings!.numCtx).toBe(4096);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -108,7 +108,7 @@ export class OllamaAiApi extends AiApi {
|
||||
!!modelInfo?.["vision_model"] ||
|
||||
!!modelInfo?.["clip"],
|
||||
hasEmbedding: capabilities.includes("embeddings"),
|
||||
hasThinking: capabilities.includes("reasoning"),
|
||||
hasThinking: capabilities.includes("thinking") || capabilities.includes("reasoning"),
|
||||
isInstructTuned:
|
||||
modelId.toLowerCase().includes("instruct") ||
|
||||
modelId.toLowerCase().includes("chat") ||
|
||||
|
||||
@ -203,6 +203,9 @@ export class OpenAiApi extends AiApi {
|
||||
{ role: "user" as const, content: options.prompt },
|
||||
],
|
||||
stream: true,
|
||||
...(typeof model.params.reasoning === "string"
|
||||
? { reasoning_effort: model.params.reasoning as "low" | "medium" | "high" }
|
||||
: {}),
|
||||
});
|
||||
|
||||
let accumulatedResponse = "";
|
||||
@ -316,6 +319,9 @@ export class OpenAiApi extends AiApi {
|
||||
messages,
|
||||
tools,
|
||||
stream: true,
|
||||
...(typeof model.params.reasoning === "string"
|
||||
? { reasoning_effort: model.params.reasoning as "low" | "medium" | "high" }
|
||||
: {}),
|
||||
});
|
||||
|
||||
let accumulatedResponse = "";
|
||||
|
||||
@ -22,6 +22,8 @@ export interface IChatSessionPin {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type ReasoningEffort = "off" | "low" | "medium" | "high";
|
||||
|
||||
export interface IChatSession {
|
||||
_id: GadgetId;
|
||||
createdAt: Date;
|
||||
@ -32,6 +34,7 @@ export interface IChatSession {
|
||||
mode: ChatSessionMode;
|
||||
provider: IAiProvider | GadgetId;
|
||||
selectedModel: string;
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
stats: {
|
||||
turnCount: number;
|
||||
toolCallCount: number;
|
||||
|
||||
@ -9,7 +9,7 @@ import type { IProject } from "./project.js";
|
||||
import type { IChatSession } from "./chat-session.js";
|
||||
import type { IAiProvider } from "./ai-provider.js";
|
||||
|
||||
import { ChatSessionMode } from "./chat-session.js";
|
||||
import { ChatSessionMode, ReasoningEffort } from "./chat-session.js";
|
||||
import { GadgetId } from "../lib/gadget-id.ts";
|
||||
|
||||
export enum ChatTurnStatus {
|
||||
@ -80,6 +80,7 @@ export interface IChatTurn {
|
||||
session: IChatSession | GadgetId;
|
||||
provider: IAiProvider | GadgetId;
|
||||
llm: string; // id/name of the model used to process the prompt
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
mode: ChatSessionMode;
|
||||
status: ChatTurnStatus;
|
||||
prompts: IChatTurnPrompts;
|
||||
|
||||
@ -112,10 +112,7 @@ export interface GadgetCodeConfig {
|
||||
audios?: string;
|
||||
};
|
||||
};
|
||||
user?: {
|
||||
signupEnabled?: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gadget Drone Worker Configuration
|
||||
|
||||
Loading…
Reference in New Issue
Block a user