app: fix false "out of date" model warnings (#15101)
The staleness check compared the local manifest digest (SHA256 of the file on disk) against the registry's Ollama-Content-Digest header. These never matched because PullModel re-serializes the manifest JSON before writing, producing different bytes than the registry's original. The fallback comparison (local modified_at vs upstream push time) was also broken: the generated TypeScript Time class discards the actual timestamp value, so Date parsing always produced NaN. Fix by moving the staleness comparison server-side where we have reliable access to both the local manifest file mtime and the upstream push time. The /api/v1/model/upstream endpoint now returns a simple `stale` boolean instead of raw digests for the frontend to compare. Also adds User-Agent to the CORS allowed headers for dev mode.
This commit is contained in:
@@ -550,14 +550,12 @@ export class Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class ModelUpstreamResponse {
|
export class ModelUpstreamResponse {
|
||||||
digest?: string;
|
stale: boolean;
|
||||||
pushTime: number;
|
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.digest = source["digest"];
|
this.stale = source["stale"];
|
||||||
this.pushTime = source["pushTime"];
|
|
||||||
this.error = source["error"];
|
this.error = source["error"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export async function getModels(query?: string): Promise<Model[]> {
|
|||||||
// Add query if it's in the registry and not already in the list
|
// Add query if it's in the registry and not already in the list
|
||||||
if (!exactMatch) {
|
if (!exactMatch) {
|
||||||
const result = await getModelUpstreamInfo(new Model({ model: query }));
|
const result = await getModelUpstreamInfo(new Model({ model: query }));
|
||||||
const existsUpstream = !!result.digest && !result.error;
|
const existsUpstream = result.exists;
|
||||||
if (existsUpstream) {
|
if (existsUpstream) {
|
||||||
filteredModels.push(new Model({ model: query }));
|
filteredModels.push(new Model({ model: query }));
|
||||||
}
|
}
|
||||||
@@ -339,7 +339,7 @@ export async function deleteChat(chatId: string): Promise<void> {
|
|||||||
// Get upstream information for model staleness checking
|
// Get upstream information for model staleness checking
|
||||||
export async function getModelUpstreamInfo(
|
export async function getModelUpstreamInfo(
|
||||||
model: Model,
|
model: Model,
|
||||||
): Promise<{ digest?: string; pushTime: number; error?: string }> {
|
): Promise<{ stale: boolean; exists: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/model/upstream`, {
|
const response = await fetch(`${API_BASE}/api/v1/model/upstream`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -353,22 +353,22 @@ export async function getModelUpstreamInfo(
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Failed to check upstream digest for ${model.model}: ${response.status}`,
|
`Failed to check upstream for ${model.model}: ${response.status}`,
|
||||||
);
|
);
|
||||||
return { pushTime: 0 };
|
return { stale: false, exists: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.warn(`Upstream digest check: ${data.error}`);
|
console.warn(`Upstream check: ${data.error}`);
|
||||||
return { error: data.error, pushTime: 0 };
|
return { stale: false, exists: false, error: data.error };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { digest: data.digest, pushTime: data.pushTime || 0 };
|
return { stale: !!data.stale, exists: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Error checking model staleness:`, error);
|
console.warn(`Error checking model staleness:`, error);
|
||||||
return { pushTime: 0 };
|
return { stale: false, exists: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,24 +61,7 @@ export const ModelPicker = forwardRef<
|
|||||||
try {
|
try {
|
||||||
const upstreamInfo = await getModelUpstreamInfo(model);
|
const upstreamInfo = await getModelUpstreamInfo(model);
|
||||||
|
|
||||||
// Compare local digest with upstream digest
|
if (upstreamInfo.stale) {
|
||||||
let isStale =
|
|
||||||
model.digest &&
|
|
||||||
upstreamInfo.digest &&
|
|
||||||
model.digest !== upstreamInfo.digest;
|
|
||||||
|
|
||||||
// If the model has a modified time and upstream has a push time,
|
|
||||||
// check if the model was modified after the push time - if so, it's not stale
|
|
||||||
if (isStale && model.modified_at && upstreamInfo.pushTime > 0) {
|
|
||||||
const modifiedAtTime =
|
|
||||||
new Date(model.modified_at as string | number | Date).getTime() /
|
|
||||||
1000;
|
|
||||||
if (modifiedAtTime > upstreamInfo.pushTime) {
|
|
||||||
isStale = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStale) {
|
|
||||||
const currentStaleModels =
|
const currentStaleModels =
|
||||||
queryClient.getQueryData<Map<string, boolean>>(["staleModels"]) ||
|
queryClient.getQueryData<Map<string, boolean>>(["staleModels"]) ||
|
||||||
new Map();
|
new Map();
|
||||||
|
|||||||
@@ -133,9 +133,8 @@ type Error struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ModelUpstreamResponse struct {
|
type ModelUpstreamResponse struct {
|
||||||
Digest string `json:"digest,omitempty"`
|
Stale bool `json:"stale"`
|
||||||
PushTime int64 `json:"pushTime"`
|
Error string `json:"error,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serializable data for the browser state
|
// Serializable data for the browser state
|
||||||
|
|||||||
18
app/ui/ui.go
18
app/ui/ui.go
@@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/ollama/ollama/app/version"
|
"github.com/ollama/ollama/app/version"
|
||||||
ollamaAuth "github.com/ollama/ollama/auth"
|
ollamaAuth "github.com/ollama/ollama/auth"
|
||||||
"github.com/ollama/ollama/envconfig"
|
"github.com/ollama/ollama/envconfig"
|
||||||
|
"github.com/ollama/ollama/manifest"
|
||||||
"github.com/ollama/ollama/types/model"
|
"github.com/ollama/ollama/types/model"
|
||||||
_ "github.com/tkrajina/typescriptify-golang-structs/typescriptify"
|
_ "github.com/tkrajina/typescriptify-golang-structs/typescriptify"
|
||||||
)
|
)
|
||||||
@@ -193,7 +194,7 @@ func (s *Server) Handler() http.Handler {
|
|||||||
if CORS() {
|
if CORS() {
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent, Accept, X-Requested-With")
|
||||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
|
||||||
// Handle preflight requests
|
// Handle preflight requests
|
||||||
@@ -318,7 +319,7 @@ func (s *Server) handleError(w http.ResponseWriter, e error) {
|
|||||||
if CORS() {
|
if CORS() {
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent, Accept, X-Requested-With")
|
||||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1572,9 +1573,18 @@ func (s *Server) modelUpstream(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return json.NewEncoder(w).Encode(response)
|
return json.NewEncoder(w).Encode(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n := model.ParseName(req.Model)
|
||||||
|
stale := true
|
||||||
|
if m, err := manifest.ParseNamedManifest(n); err == nil {
|
||||||
|
if m.Digest() == digest {
|
||||||
|
stale = false
|
||||||
|
} else if pushTime > 0 && m.FileInfo().ModTime().Unix() >= pushTime {
|
||||||
|
stale = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response := responses.ModelUpstreamResponse{
|
response := responses.ModelUpstreamResponse{
|
||||||
Digest: digest,
|
Stale: stale,
|
||||||
PushTime: pushTime,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|||||||
Reference in New Issue
Block a user