diff --git a/compose.override.yml b/compose.override.yml
index 177a19d..0949b10 100644
--- a/compose.override.yml
+++ b/compose.override.yml
@@ -8,9 +8,9 @@ services:
# Docker can follow the symlink inside the container.
- /Library/Assets/kiwi:/Library/Assets/kiwi:rw
- # cf-orch agent sidecar: registers kiwi as a GPU node with the coordinator.
+ # cf-orch agent sidecar: registers this machine as GPU node "sif" with the coordinator.
# The API scheduler uses COORDINATOR_URL to lease VRAM cooperatively; this
- # agent makes kiwi's VRAM usage visible on the orchestrator dashboard.
+ # agent makes the local VRAM usage visible on the orchestrator dashboard.
cf-orch-agent:
image: kiwi-api # reuse local api image — cf-core already installed there
network_mode: host
@@ -21,7 +21,7 @@ services:
command: >
conda run -n kiwi cf-orch agent
--coordinator ${COORDINATOR_URL:-http://10.1.10.71:7700}
- --node-id kiwi
+ --node-id sif
--host 0.0.0.0
--port 7702
--advertise-host ${CF_ORCH_ADVERTISE_HOST:-10.1.10.71}
diff --git a/frontend/src/components/RecipesView.vue b/frontend/src/components/RecipesView.vue
index fd13cb7..7671b29 100644
--- a/frontend/src/components/RecipesView.vue
+++ b/frontend/src/components/RecipesView.vue
@@ -287,7 +287,10 @@
@click="handleSuggest"
>
- Finding recipes…
+
+ Queued…
+ Generating…
+ Finding recipes…
Suggest Recipes
@@ -312,7 +315,9 @@
- Finding recipes…
+ Recipe request queued, waiting for model…
+ Generating your recipe now…
+ Finding recipes…
{{ filteredSuggestions.length }} recipe{{ filteredSuggestions.length !== 1 ? 's' : '' }} found
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index ef19ab6..acb9078 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -524,6 +524,15 @@ export interface RecipeResult {
rate_limit_count: number
}
+export type RecipeJobStatusValue = 'queued' | 'running' | 'done' | 'failed'
+
+export interface RecipeJobStatus {
+ job_id: string
+ status: RecipeJobStatusValue
+ result: RecipeResult | null
+ error: string | null
+}
+
export interface RecipeRequest {
pantry_items: string[]
secondary_pantry_items: Record
@@ -593,6 +602,18 @@ export const recipesAPI = {
const response = await api.post('/recipes/suggest', req, { timeout: 120000 })
return response.data
},
+
+ /** Submit an async job for L3/L4 generation. Returns job_id + initial status. */
+ async suggestAsync(req: RecipeRequest): Promise<{ job_id: string; status: string }> {
+ const response = await api.post('/recipes/suggest', req, { params: { async: 'true' }, timeout: 15000 })
+ return response.data
+ },
+
+ /** Poll an async job. Returns the full status including result once done. */
+ async pollJob(jobId: string): Promise {
+ const response = await api.get(`/recipes/jobs/${jobId}`, { timeout: 10000 })
+ return response.data
+ },
async getRecipe(id: number): Promise {
const response = await api.get(`/recipes/${id}`)
return response.data
diff --git a/frontend/src/stores/recipes.ts b/frontend/src/stores/recipes.ts
index 4798e17..ffff6cf 100644
--- a/frontend/src/stores/recipes.ts
+++ b/frontend/src/stores/recipes.ts
@@ -7,7 +7,7 @@
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'
-import { recipesAPI, type RecipeResult, type RecipeSuggestion, type RecipeRequest, type NutritionFilters } from '../services/api'
+import { recipesAPI, type RecipeResult, type RecipeSuggestion, type RecipeRequest, type RecipeJobStatusValue, type NutritionFilters } from '../services/api'
const DISMISSED_KEY = 'kiwi:dismissed_recipes'
const DISMISS_TTL_MS = 7 * 24 * 60 * 60 * 1000
@@ -121,6 +121,7 @@ export const useRecipesStore = defineStore('recipes', () => {
const result = ref(null)
const loading = ref(false)
const error = ref(null)
+ const jobStatus = ref(null)
// Request parameters
const level = ref(1)
@@ -199,18 +200,57 @@ export const useRecipesStore = defineStore('recipes', () => {
async function suggest(pantryItems: string[], secondaryPantryItems: Record = {}) {
loading.value = true
error.value = null
+ jobStatus.value = null
seenIds.value = new Set()
try {
- result.value = await recipesAPI.suggest(_buildRequest(pantryItems, secondaryPantryItems))
- _trackSeen(result.value.suggestions)
+ if (level.value >= 3) {
+ await _suggestAsync(pantryItems, secondaryPantryItems)
+ } else {
+ result.value = await recipesAPI.suggest(_buildRequest(pantryItems, secondaryPantryItems))
+ _trackSeen(result.value.suggestions)
+ }
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to get recipe suggestions'
} finally {
loading.value = false
+ jobStatus.value = null
}
}
+ async function _suggestAsync(pantryItems: string[], secondaryPantryItems: Record) {
+ const queued = await recipesAPI.suggestAsync(_buildRequest(pantryItems, secondaryPantryItems))
+
+ // CLOUD_MODE or future sync fallback: server returned result directly (status 200)
+ if ('suggestions' in queued) {
+ result.value = queued as unknown as RecipeResult
+ _trackSeen(result.value.suggestions)
+ return
+ }
+
+ jobStatus.value = 'queued'
+ const { job_id } = queued
+ const deadline = Date.now() + 90_000
+ const POLL_MS = 2_500
+
+ while (Date.now() < deadline) {
+ await new Promise((resolve) => setTimeout(resolve, POLL_MS))
+ const poll = await recipesAPI.pollJob(job_id)
+ jobStatus.value = poll.status
+
+ if (poll.status === 'done') {
+ result.value = poll.result
+ if (result.value) _trackSeen(result.value.suggestions)
+ return
+ }
+ if (poll.status === 'failed') {
+ throw new Error(poll.error ?? 'Recipe generation failed')
+ }
+ }
+
+ throw new Error('Recipe generation timed out — the model may be busy. Try again.')
+ }
+
async function loadMore(pantryItems: string[], secondaryPantryItems: Record = {}) {
if (!result.value || loading.value) return
loading.value = true
@@ -308,6 +348,7 @@ export const useRecipesStore = defineStore('recipes', () => {
result,
loading,
error,
+ jobStatus,
level,
constraints,
allergies,