routes_test.go 18.2 KB
Newer Older
1
2
3
package server

import (
4
	"bytes"
5
	"context"
6
	"encoding/binary"
7
8
	"encoding/json"
	"fmt"
9
	"io"
10
	"io/fs"
11
	"math"
12
13
	"math/rand/v2"
	"net"
14
15
	"net/http"
	"net/http/httptest"
16
	"os"
17
	"path/filepath"
Patrick Devine's avatar
Patrick Devine committed
18
	"sort"
19
	"strings"
20
	"testing"
21
	"unicode"
22

23
	"github.com/ollama/ollama/api"
24
	"github.com/ollama/ollama/llm"
25
	"github.com/ollama/ollama/openai"
26
	"github.com/ollama/ollama/parser"
27
	"github.com/ollama/ollama/types/model"
28
	"github.com/ollama/ollama/version"
29
30
)

31
32
func createTestFile(t *testing.T, name string) string {
	t.Helper()
33

34
	f, err := os.CreateTemp(t.TempDir(), name)
35
36
37
	if err != nil {
		t.Fatalf("failed to create temp file: %v", err)
	}
38
	defer f.Close()
39

40
	err = binary.Write(f, binary.LittleEndian, []byte("GGUF"))
41
42
43
	if err != nil {
		t.Fatalf("failed to write to file: %v", err)
	}
44

45
	err = binary.Write(f, binary.LittleEndian, uint32(3))
46
47
48
	if err != nil {
		t.Fatalf("failed to write to file: %v", err)
	}
49

50
	err = binary.Write(f, binary.LittleEndian, uint64(0))
51
52
53
	if err != nil {
		t.Fatalf("failed to write to file: %v", err)
	}
54

55
	err = binary.Write(f, binary.LittleEndian, uint64(0))
56
57
58
	if err != nil {
		t.Fatalf("failed to write to file: %v", err)
	}
59

60
61
	return f.Name()
}
62

63
64
65
66
67
68
69
70
71
72
73
74
75
// equalStringSlices checks if two slices of strings are equal.
func equalStringSlices(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

76
77
78
79
80
81
82
func Test_Routes(t *testing.T) {
	type testCase struct {
		Name     string
		Method   string
		Path     string
		Setup    func(t *testing.T, req *http.Request)
		Expected func(t *testing.T, resp *http.Response)
83
84
85
	}

	createTestModel := func(t *testing.T, name string) {
86
87
		t.Helper()

88
89
		fname := createTestFile(t, "ollama-model")

Michael Yang's avatar
Michael Yang committed
90
		r := strings.NewReader(fmt.Sprintf("FROM %s\nPARAMETER seed 42\nPARAMETER top_p 0.9\nPARAMETER stop foo\nPARAMETER stop bar", fname))
91
		modelfile, err := parser.ParseFile(r)
92
93
94
		if err != nil {
			t.Fatalf("failed to parse file: %v", err)
		}
95
96
97
		fn := func(resp api.ProgressResponse) {
			t.Logf("Status: %s", resp.Status)
		}
98
		err = CreateModel(context.TODO(), model.ParseName(name), "", "", modelfile, fn)
99
100
101
		if err != nil {
			t.Fatalf("failed to create model: %v", err)
		}
102
	}
103
104
105
106
107
108
109
110
111
112

	testCases := []testCase{
		{
			Name:   "Version Handler",
			Method: http.MethodGet,
			Path:   "/api/version",
			Setup: func(t *testing.T, req *http.Request) {
			},
			Expected: func(t *testing.T, resp *http.Response) {
				contentType := resp.Header.Get("Content-Type")
113
114
115
				if contentType != "application/json; charset=utf-8" {
					t.Errorf("expected content type application/json; charset=utf-8, got %s", contentType)
				}
116
				body, err := io.ReadAll(resp.Body)
117
118
119
120
121
122
123
				if err != nil {
					t.Fatalf("failed to read response body: %v", err)
				}
				expectedBody := fmt.Sprintf(`{"version":"%s"}`, version.Version)
				if string(body) != expectedBody {
					t.Errorf("expected body %s, got %s", expectedBody, string(body))
				}
124
125
			},
		},
126
127
128
129
130
131
		{
			Name:   "Tags Handler (no tags)",
			Method: http.MethodGet,
			Path:   "/api/tags",
			Expected: func(t *testing.T, resp *http.Response) {
				contentType := resp.Header.Get("Content-Type")
132
133
134
				if contentType != "application/json; charset=utf-8" {
					t.Errorf("expected content type application/json; charset=utf-8, got %s", contentType)
				}
135
				body, err := io.ReadAll(resp.Body)
136
137
138
				if err != nil {
					t.Fatalf("failed to read response body: %v", err)
				}
139
140
141
142

				var modelList api.ListResponse

				err = json.Unmarshal(body, &modelList)
143
144
145
				if err != nil {
					t.Fatalf("failed to unmarshal response body: %v", err)
				}
146

147
148
149
				if modelList.Models == nil || len(modelList.Models) != 0 {
					t.Errorf("expected empty model list, got %v", modelList.Models)
				}
150
151
			},
		},
152
153
154
155
156
157
		{
			Name:   "openai empty list",
			Method: http.MethodGet,
			Path:   "/v1/models",
			Expected: func(t *testing.T, resp *http.Response) {
				contentType := resp.Header.Get("Content-Type")
158
159
160
				if contentType != "application/json" {
					t.Errorf("expected content type application/json, got %s", contentType)
				}
161
				body, err := io.ReadAll(resp.Body)
162
163
164
				if err != nil {
					t.Fatalf("failed to read response body: %v", err)
				}
165
166
167

				var modelList openai.ListCompletion
				err = json.Unmarshal(body, &modelList)
168
169
170
				if err != nil {
					t.Fatalf("failed to unmarshal response body: %v", err)
				}
171

172
173
174
				if modelList.Object != "list" || len(modelList.Data) != 0 {
					t.Errorf("expected empty model list, got %v", modelList.Data)
				}
175
176
			},
		},
177
178
179
180
181
		{
			Name:   "Tags Handler (yes tags)",
			Method: http.MethodGet,
			Path:   "/api/tags",
			Setup: func(t *testing.T, req *http.Request) {
182
				createTestModel(t, "test-model")
183
184
185
			},
			Expected: func(t *testing.T, resp *http.Response) {
				contentType := resp.Header.Get("Content-Type")
186
187
188
				if contentType != "application/json; charset=utf-8" {
					t.Errorf("expected content type application/json; charset=utf-8, got %s", contentType)
				}
189
				body, err := io.ReadAll(resp.Body)
190
191
192
				if err != nil {
					t.Fatalf("failed to read response body: %v", err)
				}
193

194
195
196
				if strings.Contains(string(body), "expires_at") {
					t.Errorf("response body should not contain 'expires_at'")
				}
197

198
199
				var modelList api.ListResponse
				err = json.Unmarshal(body, &modelList)
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
				if err != nil {
					t.Fatalf("failed to unmarshal response body: %v", err)
				}

				if len(modelList.Models) != 1 || modelList.Models[0].Name != "test-model:latest" {
					t.Errorf("expected model 'test-model:latest', got %v", modelList.Models)
				}
			},
		},
		{
			Name:   "Delete Model Handler",
			Method: http.MethodDelete,
			Path:   "/api/delete",
			Setup: func(t *testing.T, req *http.Request) {
				createTestModel(t, "model-to-delete")

				deleteReq := api.DeleteRequest{
					Name: "model-to-delete",
				}
				jsonData, err := json.Marshal(deleteReq)
				if err != nil {
					t.Fatalf("failed to marshal delete request: %v", err)
				}
223

224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
				req.Body = io.NopCloser(bytes.NewReader(jsonData))
			},
			Expected: func(t *testing.T, resp *http.Response) {
				if resp.StatusCode != http.StatusOK {
					t.Errorf("expected status code 200, got %d", resp.StatusCode)
				}

				// Verify the model was deleted
				_, err := GetModel("model-to-delete")
				if err == nil || !os.IsNotExist(err) {
					t.Errorf("expected model to be deleted, got error %v", err)
				}
			},
		},
		{
			Name:   "Delete Non-existent Model",
			Method: http.MethodDelete,
			Path:   "/api/delete",
			Setup: func(t *testing.T, req *http.Request) {
				deleteReq := api.DeleteRequest{
					Name: "non-existent-model",
				}
				jsonData, err := json.Marshal(deleteReq)
				if err != nil {
					t.Fatalf("failed to marshal delete request: %v", err)
				}

				req.Body = io.NopCloser(bytes.NewReader(jsonData))
			},
			Expected: func(t *testing.T, resp *http.Response) {
				if resp.StatusCode != http.StatusNotFound {
					t.Errorf("expected status code 404, got %d", resp.StatusCode)
				}

				body, err := io.ReadAll(resp.Body)
				if err != nil {
					t.Fatalf("failed to read response body: %v", err)
				}

				var errorResp map[string]string
				err = json.Unmarshal(body, &errorResp)
				if err != nil {
					t.Fatalf("failed to unmarshal response body: %v", err)
				}

				if !strings.Contains(errorResp["error"], "not found") {
					t.Errorf("expected error message to contain 'not found', got %s", errorResp["error"])
				}
272
273
			},
		},
274
275
276
277
278
279
		{
			Name:   "openai list models with tags",
			Method: http.MethodGet,
			Path:   "/v1/models",
			Expected: func(t *testing.T, resp *http.Response) {
				contentType := resp.Header.Get("Content-Type")
280
281
282
				if contentType != "application/json" {
					t.Errorf("expected content type application/json, got %s", contentType)
				}
283
				body, err := io.ReadAll(resp.Body)
284
285
286
				if err != nil {
					t.Fatalf("failed to read response body: %v", err)
				}
287
288
289

				var modelList openai.ListCompletion
				err = json.Unmarshal(body, &modelList)
290
291
292
				if err != nil {
					t.Fatalf("failed to unmarshal response body: %v", err)
				}
293

294
295
296
				if len(modelList.Data) != 1 || modelList.Data[0].Id != "test-model:latest" || modelList.Data[0].OwnedBy != "library" {
					t.Errorf("expected model 'test-model:latest' owned by 'library', got %v", modelList.Data)
				}
297
298
			},
		},
299
300
301
302
303
		{
			Name:   "Create Model Handler",
			Method: http.MethodPost,
			Path:   "/api/create",
			Setup: func(t *testing.T, req *http.Request) {
Michael Yang's avatar
Michael Yang committed
304
				fname := createTestFile(t, "ollama-model")
305
306
307
308

				stream := false
				createReq := api.CreateRequest{
					Name:      "t-bone",
Michael Yang's avatar
Michael Yang committed
309
					Modelfile: fmt.Sprintf("FROM %s", fname),
310
311
312
					Stream:    &stream,
				}
				jsonData, err := json.Marshal(createReq)
313
314
315
				if err != nil {
					t.Fatalf("failed to marshal create request: %v", err)
				}
316
317
318
319
320

				req.Body = io.NopCloser(bytes.NewReader(jsonData))
			},
			Expected: func(t *testing.T, resp *http.Response) {
				contentType := resp.Header.Get("Content-Type")
321
322
323
				if contentType != "application/json" {
					t.Errorf("expected content type application/json, got %s", contentType)
				}
324
				_, err := io.ReadAll(resp.Body)
325
326
327
328
329
330
				if err != nil {
					t.Fatalf("failed to read response body: %v", err)
				}
				if resp.StatusCode != http.StatusOK { // Updated line
					t.Errorf("expected status code 200, got %d", resp.StatusCode)
				}
331
332

				model, err := GetModel("t-bone")
333
334
335
336
337
338
				if err != nil {
					t.Fatalf("failed to get model: %v", err)
				}
				if model.ShortName != "t-bone:latest" {
					t.Errorf("expected model name 't-bone:latest', got %s", model.ShortName)
				}
339
340
341
342
343
344
345
346
347
348
349
350
351
			},
		},
		{
			Name:   "Copy Model Handler",
			Method: http.MethodPost,
			Path:   "/api/copy",
			Setup: func(t *testing.T, req *http.Request) {
				createTestModel(t, "hamshank")
				copyReq := api.CopyRequest{
					Source:      "hamshank",
					Destination: "beefsteak",
				}
				jsonData, err := json.Marshal(copyReq)
352
353
354
				if err != nil {
					t.Fatalf("failed to marshal copy request: %v", err)
				}
355
356
357
358
359

				req.Body = io.NopCloser(bytes.NewReader(jsonData))
			},
			Expected: func(t *testing.T, resp *http.Response) {
				model, err := GetModel("beefsteak")
360
361
362
363
364
365
				if err != nil {
					t.Fatalf("failed to get model: %v", err)
				}
				if model.ShortName != "beefsteak:latest" {
					t.Errorf("expected model name 'beefsteak:latest', got %s", model.ShortName)
				}
366
367
			},
		},
Patrick Devine's avatar
Patrick Devine committed
368
369
370
371
372
373
374
375
		{
			Name:   "Show Model Handler",
			Method: http.MethodPost,
			Path:   "/api/show",
			Setup: func(t *testing.T, req *http.Request) {
				createTestModel(t, "show-model")
				showReq := api.ShowRequest{Model: "show-model"}
				jsonData, err := json.Marshal(showReq)
376
377
378
				if err != nil {
					t.Fatalf("failed to marshal show request: %v", err)
				}
Patrick Devine's avatar
Patrick Devine committed
379
380
381
382
				req.Body = io.NopCloser(bytes.NewReader(jsonData))
			},
			Expected: func(t *testing.T, resp *http.Response) {
				contentType := resp.Header.Get("Content-Type")
383
384
385
				if contentType != "application/json; charset=utf-8" {
					t.Errorf("expected content type application/json; charset=utf-8, got %s", contentType)
				}
Patrick Devine's avatar
Patrick Devine committed
386
				body, err := io.ReadAll(resp.Body)
387
388
389
				if err != nil {
					t.Fatalf("failed to read response body: %v", err)
				}
Patrick Devine's avatar
Patrick Devine committed
390
391
392

				var showResp api.ShowResponse
				err = json.Unmarshal(body, &showResp)
393
394
395
				if err != nil {
					t.Fatalf("failed to unmarshal response body: %v", err)
				}
Patrick Devine's avatar
Patrick Devine committed
396
397
398
399
400
401
402
403
404
405
406
407
408

				var params []string
				paramsSplit := strings.Split(showResp.Parameters, "\n")
				for _, p := range paramsSplit {
					params = append(params, strings.Join(strings.Fields(p), " "))
				}
				sort.Strings(params)
				expectedParams := []string{
					"seed 42",
					"stop \"bar\"",
					"stop \"foo\"",
					"top_p 0.9",
				}
409
410
411
412
413
414
415
416
417
418
				if !equalStringSlices(params, expectedParams) {
					t.Errorf("expected parameters %v, got %v", expectedParams, params)
				}
				paramCount, ok := showResp.ModelInfo["general.parameter_count"].(float64)
				if !ok {
					t.Fatalf("expected parameter count to be a float64, got %T", showResp.ModelInfo["general.parameter_count"])
				}
				if math.Abs(paramCount) > 1e-9 {
					t.Errorf("expected parameter count to be 0, got %f", paramCount)
				}
Patrick Devine's avatar
Patrick Devine committed
419
420
			},
		},
421
422
423
424
425
426
		{
			Name:   "openai retrieve model handler",
			Method: http.MethodGet,
			Path:   "/v1/models/show-model",
			Expected: func(t *testing.T, resp *http.Response) {
				contentType := resp.Header.Get("Content-Type")
427
428
429
				if contentType != "application/json" {
					t.Errorf("expected content type application/json, got %s", contentType)
				}
430
				body, err := io.ReadAll(resp.Body)
431
432
433
				if err != nil {
					t.Fatalf("failed to read response body: %v", err)
				}
434
435
436

				var retrieveResp api.RetrieveModelResponse
				err = json.Unmarshal(body, &retrieveResp)
437
438
439
				if err != nil {
					t.Fatalf("failed to unmarshal response body: %v", err)
				}
440

441
442
443
				if retrieveResp.Id != "show-model" || retrieveResp.OwnedBy != "library" {
					t.Errorf("expected model 'show-model' owned by 'library', got %v", retrieveResp)
				}
444
445
			},
		},
446
447
	}

448
449
	t.Setenv("OLLAMA_MODELS", t.TempDir())

450
	s := &Server{}
451
452
453
454
455
456
	router := s.GenerateRoutes()

	httpSrv := httptest.NewServer(router)
	t.Cleanup(httpSrv.Close)

	for _, tc := range testCases {
Michael Yang's avatar
Michael Yang committed
457
458
459
		t.Run(tc.Name, func(t *testing.T) {
			u := httpSrv.URL + tc.Path
			req, err := http.NewRequestWithContext(context.TODO(), tc.Method, u, nil)
460
461
462
			if err != nil {
				t.Fatalf("failed to create request: %v", err)
			}
Michael Yang's avatar
Michael Yang committed
463
464
465
466
467
468

			if tc.Setup != nil {
				tc.Setup(t, req)
			}

			resp, err := httpSrv.Client().Do(req)
469
470
471
			if err != nil {
				t.Fatalf("failed to do request: %v", err)
			}
Michael Yang's avatar
Michael Yang committed
472
473
474
475
476
477
			defer resp.Body.Close()

			if tc.Expected != nil {
				tc.Expected(t, resp)
			}
		})
478
479
	}
}
480

481
482
483
484
485
486
487
488
func casingShuffle(s string) string {
	rr := []rune(s)
	for i := range rr {
		if rand.N(2) == 0 {
			rr[i] = unicode.ToUpper(rr[i])
		} else {
			rr[i] = unicode.ToLower(rr[i])
		}
489
	}
490
491
	return string(rr)
}
492

493
494
func TestManifestCaseSensitivity(t *testing.T) {
	t.Setenv("OLLAMA_MODELS", t.TempDir())
495

496
497
498
499
500
501
502
503
504
505
506
507
508
	r := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		io.WriteString(w, `{}`) //nolint:errcheck
	}))
	defer r.Close()

	nameUsed := make(map[string]bool)
	name := func() string {
		const fqmn = "example/namespace/model:tag"
		for {
			v := casingShuffle(fqmn)
			if nameUsed[v] {
				continue
509
			}
510
511
512
513
			nameUsed[v] = true
			return v
		}
	}
514

515
	wantStableName := name()
516

517
518
	t.Logf("stable name: %s", wantStableName)

519
520
521
522
	// checkManifestList tests that there is strictly one manifest in the
	// models directory, and that the manifest is for the model under test.
	checkManifestList := func() {
		t.Helper()
523

524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
		mandir := filepath.Join(os.Getenv("OLLAMA_MODELS"), "manifests/")
		var entries []string
		t.Logf("dir entries:")
		fsys := os.DirFS(mandir)
		err := fs.WalkDir(fsys, ".", func(path string, info fs.DirEntry, err error) error {
			if err != nil {
				return err
			}
			t.Logf("    %s", fs.FormatDirEntry(info))
			if info.IsDir() {
				return nil
			}
			path = strings.TrimPrefix(path, mandir)
			entries = append(entries, path)
			return nil
		})
		if err != nil {
			t.Fatalf("failed to walk directory: %v", err)
		}
543

544
545
546
547
		if len(entries) != 1 {
			t.Errorf("len(got) = %d, want 1", len(entries))
			return // do not use Fatal so following steps run
		}
548

549
550
551
552
553
554
555
556
		g := entries[0] // raw path
		g = filepath.ToSlash(g)
		w := model.ParseName(wantStableName).Filepath()
		w = filepath.ToSlash(w)
		if g != w {
			t.Errorf("\ngot:  %s\nwant: %s", g, w)
		}
	}
557

558
559
560
561
562
563
564
	checkOK := func(w *httptest.ResponseRecorder) {
		t.Helper()
		if w.Code != http.StatusOK {
			t.Errorf("code = %d, want 200", w.Code)
			t.Logf("body: %s", w.Body.String())
		}
	}
565

566
567
568
569
	var s Server
	testMakeRequestDialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
		var d net.Dialer
		return d.DialContext(ctx, "tcp", r.Listener.Addr().String())
570
	}
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
	t.Cleanup(func() { testMakeRequestDialContext = nil })

	t.Logf("creating")
	checkOK(createRequest(t, s.CreateHandler, api.CreateRequest{
		// Start with the stable name, and later use a case-shuffled
		// version.
		Name: wantStableName,

		Modelfile: fmt.Sprintf("FROM %s", createBinFile(t, nil, nil)),
		Stream:    &stream,
	}))
	checkManifestList()

	t.Logf("creating (again)")
	checkOK(createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:      name(),
		Modelfile: fmt.Sprintf("FROM %s", createBinFile(t, nil, nil)),
		Stream:    &stream,
	}))
	checkManifestList()

	t.Logf("pulling")
	checkOK(createRequest(t, s.PullHandler, api.PullRequest{
		Name:     name(),
		Stream:   &stream,
		Insecure: true,
	}))
	checkManifestList()

	t.Logf("copying")
	checkOK(createRequest(t, s.CopyHandler, api.CopyRequest{
		Source:      name(),
		Destination: name(),
	}))
	checkManifestList()
606
607
608
609
610
611
612
613
614
615
616
617

	t.Logf("pushing")
	rr := createRequest(t, s.PushHandler, api.PushRequest{
		Model:    name(),
		Insecure: true,
		Username: "alice",
		Password: "x",
	})
	checkOK(rr)
	if !strings.Contains(rr.Body.String(), `"status":"success"`) {
		t.Errorf("got = %q, want success", rr.Body.String())
	}
618
}
619
620
621
622
623
624

func TestShow(t *testing.T) {
	t.Setenv("OLLAMA_MODELS", t.TempDir())

	var s Server

625
	createRequest(t, s.CreateHandler, api.CreateRequest{
626
627
628
629
		Name: "show-model",
		Modelfile: fmt.Sprintf(
			"FROM %s\nFROM %s",
			createBinFile(t, llm.KV{"general.architecture": "test"}, nil),
630
			createBinFile(t, llm.KV{"general.type": "projector", "general.architecture": "clip"}, nil),
631
632
633
		),
	})

634
	w := createRequest(t, s.ShowHandler, api.ShowRequest{
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
		Name: "show-model",
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	var resp api.ShowResponse
	if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
		t.Fatal(err)
	}

	if resp.ModelInfo["general.architecture"] != "test" {
		t.Fatal("Expected model architecture to be 'test', but got", resp.ModelInfo["general.architecture"])
	}

	if resp.ProjectorInfo["general.architecture"] != "clip" {
		t.Fatal("Expected projector architecture to be 'clip', but got", resp.ProjectorInfo["general.architecture"])
	}
}
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689

func TestNormalize(t *testing.T) {
	type testCase struct {
		input []float32
	}

	testCases := []testCase{
		{input: []float32{1}},
		{input: []float32{0, 1, 2, 3}},
		{input: []float32{0.1, 0.2, 0.3}},
		{input: []float32{-0.1, 0.2, 0.3, -0.4}},
		{input: []float32{0, 0, 0}},
	}

	isNormalized := func(vec []float32) (res bool) {
		sum := 0.0
		for _, v := range vec {
			sum += float64(v * v)
		}
		if math.Abs(sum-1) > 1e-6 {
			return sum == 0
		} else {
			return true
		}
	}

	for _, tc := range testCases {
		t.Run("", func(t *testing.T) {
			normalized := normalize(tc.input)
			if !isNormalized(normalized) {
				t.Errorf("Vector %v is not normalized", tc.input)
			}
		})
	}
}